Refactored the LintLocalizableStrings

Added inline errors & warnings (regex could use some work to remove invalid cases)
Added a build step to validate the strings are included in the app and it's extensions
This commit is contained in:
Morgan Pretty 2023-09-25 18:32:56 +10:00
parent bfc5375a30
commit 5917cf103f
89 changed files with 893 additions and 605 deletions

View File

@ -1,4 +1,6 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import CryptoKit

View File

@ -1,5 +1,11 @@
#!/usr/bin/env xcrun --sdk macosx swift
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
//
// This script is used to generate/update the set of Emoji used for reactions
//
// stringlint:disable
import Foundation
// OWSAssertionError but for this script
@ -250,6 +256,7 @@ extension EmojiGenerator {
// e.g. case grinning = "😀"
writeBlock(fileName: "Emoji.swift") { fileHandle in
fileHandle.writeLine("// swiftlint:disable all")
fileHandle.writeLine("// stringlint:disable")
fileHandle.writeLine("")
fileHandle.writeLine("/// A sorted representation of all available emoji")
fileHandle.writeLine("enum Emoji: String, CaseIterable, Equatable {")
@ -306,6 +313,9 @@ extension EmojiGenerator {
// if rawValue == "😀" { self.init(baseEmoji: .grinning, skinTones: nil) }
// else if rawValue == "🦻🏻" { self.init(baseEmoji: .earWithHearingAid, skinTones: [.light])
writeBlock(fileName: "EmojiWithSkinTones+String.swift") { fileHandle in
fileHandle.writeLine("// swiftlint:disable all")
fileHandle.writeLine("// stringlint:disable")
fileHandle.writeLine("")
fileHandle.writeLine("extension EmojiWithSkinTones {")
fileHandle.indent {
switch desiredStructure {
@ -372,6 +382,7 @@ extension EmojiGenerator {
}
}
fileHandle.writeLine("}")
fileHandle.writeLine("// swiftlint:disable all")
}
}
@ -436,6 +447,9 @@ extension EmojiGenerator {
static func writeSkinToneLookupFile(from emojiModel: EmojiModel) {
writeBlock(fileName: "Emoji+SkinTones.swift") { fileHandle in
fileHandle.writeLine("// swiftlint:disable all")
fileHandle.writeLine("// stringlint:disable")
fileHandle.writeLine("")
fileHandle.writeLine("extension Emoji {")
fileHandle.indent {
// SkinTone enum
@ -498,6 +512,7 @@ extension EmojiGenerator {
fileHandle.writeLine("}")
}
fileHandle.writeLine("}")
fileHandle.writeLine("// swiftlint:disable all")
}
}
@ -514,6 +529,9 @@ extension EmojiGenerator {
]
writeBlock(fileName: "Emoji+Category.swift") { fileHandle in
fileHandle.writeLine("// swiftlint:disable all")
fileHandle.writeLine("// stringlint:disable")
fileHandle.writeLine("")
fileHandle.writeLine("extension Emoji {")
fileHandle.indent {
@ -619,6 +637,7 @@ extension EmojiGenerator {
fileHandle.writeLine("}")
}
fileHandle.writeLine("}")
fileHandle.writeLine("// swiftlint:disable all")
}
}
@ -626,6 +645,9 @@ extension EmojiGenerator {
// Name lookup: Create a computed property mapping an Emoji enum element to the raw Emoji name string
// e.g. case .grinning: return "GRINNING FACE"
writeBlock(fileName: "Emoji+Name.swift") { fileHandle in
fileHandle.writeLine("// swiftlint:disable all")
fileHandle.writeLine("// stringlint:disable")
fileHandle.writeLine("")
fileHandle.writeLine("extension Emoji {")
fileHandle.indent {
fileHandle.writeLine("var name: String {")
@ -639,6 +661,7 @@ extension EmojiGenerator {
fileHandle.writeLine("}")
}
fileHandle.writeLine("}")
fileHandle.writeLine("// swiftlint:disable all")
}
}
}

View File

@ -1,264 +1,556 @@
#!/usr/bin/xcrun --sdk macosx swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
//
// This script is based on https://github.com/ginowu7/CleanSwiftLocalizableExample the main difference
// is canges to the localized usage regex
//
// stringlint:disable
import Foundation
let fileManager = FileManager.default
let currentPath = (
ProcessInfo.processInfo.environment["PROJECT_DIR"] ?? fileManager.currentDirectoryPath
extension ProjectState {
/// Adding `// stringlint:disable` to the top of a source file (before imports) or after a string will mean that file/line gets
/// ignored by this script (good for some things like the auto-generated emoji strings or debug strings)
static let lintSuppression: String = "stringlint:disable"
static let primaryLocalisationFile: String = "en"
static let validLocalisationSuffixes: Set<String> = ["Localizable.strings"]
static let validSourceSuffixes: Set<String> = [".swift", ".m"]
static let excludedPaths: Set<String> = [
"build/", // Files under the build folder (CI)
"Pods/", // The pods folder
"Protos/", // The protobuf files
".xcassets/", // Asset bundles
".app/", // App build directories
".appex/", // Extension build directories
"tests/", // Exclude test directories
"_SharedTestUtilities/", // Exclude shared test directory
"external/" // External dependencies
]
static let excludedPhrases: Set<String> = [ "", " ", ",", ", ", "null" ]
static let excludedUnlocalisedStringLineMatching: Set<MatchType> = [
.contains(ProjectState.lintSuppression),
.prefix("#import"),
.prefix("@available("),
.contains("fatalError("),
.contains("precondition("),
.contains("preconditionFailure("),
.contains("print("),
.contains("NSLog("),
.contains("SNLog("),
.contains("owsFailDebug("),
.contains("#imageLiteral(resourceName:"),
.contains("UIImage(named:"),
.contains("UIImage(systemName:"),
.contains("[UIImage imageNamed:"),
.contains("UIFont(name:"),
.contains(".accessibilityLabel ="),
.contains(".accessibilityIdentifier ="),
.contains("accessibilityIdentifier:"),
.contains("accessibilityLabel:"),
.contains("Accessibility(identifier:"),
.contains("Accessibility(label:"),
.containsAnd("identifier:", .previousLine(numEarlier: 1, .contains("Accessibility("))),
.containsAnd("label:", .previousLine(numEarlier: 1, .contains("Accessibility("))),
.containsAnd("label:", .previousLine(numEarlier: 2, .contains("Accessibility("))),
.contains("SQL("),
.regex(".*static var databaseTableName: String"),
.regex("Logger\\..*\\("),
.regex("OWSLogger\\..*\\("),
.regex("case .* = ")
]
}
// Execute the desired actions
let targetActions: Set<ScriptAction> = {
let args = CommandLine.arguments
// The first argument is the file name
guard args.count > 1 else { return [.lintStrings] }
return Set(args.suffix(from: 1).map { (ScriptAction(rawValue: $0) ?? .lintStrings) })
}()
print("------------ Searching Through Files ------------")
let projectState: ProjectState = ProjectState(
path: (
ProcessInfo.processInfo.environment["PROJECT_DIR"] ??
FileManager.default.currentDirectoryPath
),
loadSourceFiles: targetActions.contains(.lintStrings)
)
print("------------ Found \(projectState.localizationFiles.count) Localization File(s) ------------")
targetActions.forEach { $0.perform(projectState: projectState) }
/// List of files in currentPath - recursive
var pathFiles: [String] = {
guard
let enumerator: FileManager.DirectoryEnumerator = fileManager.enumerator(
at: URL(fileURLWithPath: currentPath),
includingPropertiesForKeys: [.isDirectoryKey],
options: [.skipsHiddenFiles]
),
let fileUrls: [URL] = enumerator.allObjects as? [URL]
else { fatalError("Could not locate files in path directory: \(currentPath)") }
// MARK: - ScriptAction
enum ScriptAction: String {
case validateFilesCopied = "validate"
case lintStrings = "lint"
return fileUrls
.filter {
((try? $0.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == false) && // No directories
!$0.path.contains("build/") && // Exclude files under the build folder (CI)
!$0.path.contains("Pods/") && // Exclude files under the pods folder
!$0.path.contains(".xcassets") && // Exclude asset bundles
!$0.path.contains(".app/") && // Exclude files in the app build directories
!$0.path.contains(".appex/") && // Exclude files in the extension build directories
!$0.path.localizedCaseInsensitiveContains("tests/") && // Exclude files under test directories
!$0.path.localizedCaseInsensitiveContains("external/") && ( // Exclude files under external directories
// Only include relevant files
$0.path.hasSuffix("Localizable.strings") ||
NSString(string: $0.path).pathExtension == "swift" ||
NSString(string: $0.path).pathExtension == "m"
)
}
.map { $0.path }
}()
/// List of localizable files - not including Localizable files in the Pods
var localizableFiles: [String] = {
return pathFiles.filter { $0.hasSuffix("Localizable.strings") }
}()
/// List of executable files
var executableFiles: [String] = {
return pathFiles.filter {
$0.hasSuffix(".swift") ||
$0.hasSuffix(".m")
}
}()
/// Reads contents in path
///
/// - Parameter path: path of file
/// - Returns: content in file
func contents(atPath path: String) -> String {
guard let data = fileManager.contents(atPath: path), let content = String(data: data, encoding: .utf8) else {
fatalError("Could not read from path: \(path)")
}
return content
}
/// Returns a list of strings that match regex pattern from content
///
/// - Parameters:
/// - pattern: regex pattern
/// - content: content to match
/// - Returns: list of results
func regexFor(_ pattern: String, content: String) -> [String] {
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
fatalError("Regex not formatted correctly: \(pattern)")
}
let matches = regex.matches(in: content, options: [], range: NSRange(location: 0, length: content.utf16.count))
return matches.map {
guard let range = Range($0.range(at: 0), in: content) else {
fatalError("Incorrect range match")
}
return String(content[range])
}
}
func create() -> [LocalizationStringsFile] {
return localizableFiles.map(LocalizationStringsFile.init(path:))
}
///
///
/// - Returns: A list of LocalizationCodeFile - contains path of file and all keys in it
func localizedStringsInCode() -> [LocalizationCodeFile] {
return executableFiles.compactMap {
let content = contents(atPath: $0)
// Note: Need to exclude escaped quotation marks from strings
let matchesOld = regexFor("(?<=NSLocalizedString\\()\\s*\"(?!.*?%d)(.*?)\"", content: content)
let matchesNew = regexFor("\"(?!.*?%d)([^(\\\")]*?)\"(?=\\s*)(?=\\.localized)", content: content)
let allMatches = (matchesOld + matchesNew)
return allMatches.isEmpty ? nil : LocalizationCodeFile(path: $0, keys: Set(allMatches))
}
}
/// Throws error if ALL localizable files does not have matching keys
///
/// - Parameter files: list of localizable files to validate
func validateMatchKeys(_ files: [LocalizationStringsFile]) {
guard let base = files.first, files.count > 1 else { return }
let files = Array(files.dropFirst())
files.forEach {
guard let extraKey = Set(base.keys).symmetricDifference($0.keys).first else { return }
let incorrectFile = $0.keys.contains(extraKey) ? $0 : base
printPretty("error: Found extra key: \(extraKey) in file: \(incorrectFile.path)")
}
}
/// Throws error if localizable files are missing keys
///
/// - Parameters:
/// - codeFiles: Array of LocalizationCodeFile
/// - localizationFiles: Array of LocalizableStringFiles
func validateMissingKeys(_ codeFiles: [LocalizationCodeFile], localizationFiles: [LocalizationStringsFile]) {
guard let baseFile = localizationFiles.first else {
fatalError("Could not locate base localization file")
}
let baseKeys = Set(baseFile.keys)
codeFiles.forEach {
let extraKeys = $0.keys.subtracting(baseKeys)
if !extraKeys.isEmpty {
printPretty("error: Found keys in code missing in strings file: \(extraKeys) from \($0.path)")
}
}
}
/// Throws warning if keys exist in localizable file but are not being used
///
/// - Parameters:
/// - codeFiles: Array of LocalizationCodeFile
/// - localizationFiles: Array of LocalizableStringFiles
func validateDeadKeys(_ codeFiles: [LocalizationCodeFile], localizationFiles: [LocalizationStringsFile]) {
guard let baseFile = localizationFiles.first else {
fatalError("Could not locate base localization file")
}
let baseKeys: Set<String> = Set(baseFile.keys)
let allCodeFileKeys: [String] = codeFiles.flatMap { $0.keys }
let deadKeys: [String] = Array(baseKeys.subtracting(allCodeFileKeys))
.sorted()
.map { $0.trimmingCharacters(in: CharacterSet(charactersIn: "\"")) }
if !deadKeys.isEmpty {
printPretty("warning: \(deadKeys) - Suggest cleaning dead keys")
}
}
protocol Pathable {
var path: String { get }
}
struct LocalizationStringsFile: Pathable {
let path: String
let kv: [String: String]
let duplicates: [(key: String, path: String)]
var keys: [String] {
return Array(kv.keys)
}
init(path: String) {
let result = ContentParser.parse(path)
self.path = path
self.kv = result.kv
self.duplicates = result.duplicates
}
/// Writes back to localizable file with sorted keys and removed whitespaces and new lines
func cleanWrite() {
print("------------ Sort and remove whitespaces: \(path) ------------")
let content = kv.keys.sorted().map { "\($0) = \(kv[$0]!);" }.joined(separator: "\n")
try! content.write(toFile: path, atomically: true, encoding: .utf8)
}
}
struct LocalizationCodeFile: Pathable {
let path: String
let keys: Set<String>
}
struct ContentParser {
/// Parses contents of a file to localizable keys and values - Throws error if localizable file have duplicated keys
///
/// - Parameter path: Localizable file paths
/// - Returns: localizable key and value for content at path
static func parse(_ path: String) -> (kv: [String: String], duplicates: [(key: String, path: String)]) {
let content = contents(atPath: path)
let trimmed = content
.replacingOccurrences(of: "\n+", with: "", options: .regularExpression, range: nil)
.trimmingCharacters(in: .whitespacesAndNewlines)
let keys = regexFor("\"([^\"]*?)\"(?= =)", content: trimmed)
let values = regexFor("(?<== )\"(.*?)\"(?=;)", content: trimmed)
if keys.count != values.count {
fatalError("Error parsing contents: Make sure all keys and values are in correct format (this could be due to extra spaces between keys and values)")
}
var duplicates: [(key: String, path: String)] = []
let kv: [String: String] = zip(keys, values)
.reduce(into: [:]) { results, keyValue in
guard results[keyValue.0] == nil else {
duplicates.append((keyValue.0, path))
return
func perform(projectState: ProjectState) {
// Perform the action
switch self {
case .validateFilesCopied:
print("------------ Checking Copied Files ------------")
guard
let builtProductsPath: String = ProcessInfo.processInfo.environment["BUILT_PRODUCTS_DIR"],
let productName: String = ProcessInfo.processInfo.environment["FULL_PRODUCT_NAME"],
let enumerator: FileManager.DirectoryEnumerator = FileManager.default.enumerator(
at: URL(fileURLWithPath: "\(builtProductsPath)/\(productName)"),
includingPropertiesForKeys: [.isDirectoryKey],
options: [.skipsHiddenFiles]
),
let fileUrls: [URL] = enumerator.allObjects as? [URL]
else { return Output.error("Could not retrieve list of files within built product") }
let localizationFiles: Set<String> = Set(fileUrls
.filter { $0.path.hasSuffix(".lproj") }
.map { $0.lastPathComponent.replacingOccurrences(of: ".lproj", with: "") })
let missingFiles: Set<String> = Set(projectState.localizationFiles
.map { $0.name })
.subtracting(localizationFiles)
guard missingFiles.isEmpty else {
return Output.error("Translations missing from \(productName): \(missingFiles.joined(separator: ", "))")
}
break
case .lintStrings:
guard !projectState.localizationFiles.isEmpty else {
return print("------------ Nothing to lint ------------")
}
results[keyValue.0] = keyValue.1
}
print("------------ Processing \(projectState.localizationFiles.count) Localization File(s) ------------")
// Add warnings for any duplicate keys
projectState.localizationFiles.forEach { file in
// Show errors for any duplicates
file.duplicates.forEach { phrase, original in Output.duplicate(phrase, original: original) }
// Show warnings for any phrases missing from the file
let allKeys: Set<String> = Set(file.keyPhrase.keys)
let missingKeysFromOtherFiles: [String: [String]] = projectState.localizationFiles.reduce(into: [:]) { result, otherFile in
guard otherFile.path != file.path else { return }
let missingKeys: Set<String> = Set(otherFile.keyPhrase.keys)
.subtracting(allKeys)
missingKeys.forEach { missingKey in
result[missingKey] = ((result[missingKey] ?? []) + [otherFile.name])
}
}
missingKeysFromOtherFiles.forEach { missingKey, namesOfFilesItWasFound in
Output.warning(file, "Phrase '\(missingKey)' is missing (found in: \(namesOfFilesItWasFound.joined(separator: ", ")))")
}
}
// Process the source code
print("------------ Processing \(projectState.sourceFiles.count) Source File(s) ------------")
let allKeys: Set<String> = Set(projectState.primaryLocalizationFile.keyPhrase.keys)
projectState.sourceFiles.forEach { file in
// Add logs for unlocalised strings
file.unlocalizedPhrases.forEach { phrase in
Output.warning(phrase, "Found unlocalized string '\(phrase.key)'")
}
// Add errors for missing localised strings
let missingKeys: Set<String> = Set(file.keyPhrase.keys).subtracting(allKeys)
missingKeys.forEach { key in
switch file.keyPhrase[key] {
case .some(let phrase): Output.error(phrase, "Localized phrase '\(key)' missing from strings files")
case .none: Output.error(file, "Localized phrase '\(key)' missing from strings files")
}
}
}
break
}
return (kv, duplicates)
print("------------ Complete ------------")
}
}
func printPretty(_ string: String) {
print(string.replacingOccurrences(of: "\\", with: ""))
// MARK: - Functionality
enum Regex {
/// Returns a list of strings that match regex pattern from content
///
/// - Parameters:
/// - pattern: regex pattern
/// - content: content to match
/// - Returns: list of results
static func matches(_ pattern: String, content: String) -> [String] {
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
fatalError("Regex not formatted correctly: \(pattern)")
}
let matches = regex.matches(in: content, options: [], range: NSRange(location: 0, length: content.utf16.count))
return matches.map {
guard let range = Range($0.range(at: 0), in: content) else {
fatalError("Incorrect range match")
}
return String(content[range])
}
}
}
// MARK: - Processing
// MARK: - Output
let stringFiles: [LocalizationStringsFile] = create()
if !stringFiles.isEmpty {
print("------------ Found \(stringFiles.count) file(s) - checking for duplicate, extra, missing and dead keys ------------")
enum Output {
static func error(_ error: String) {
print("error: \(error)")
}
stringFiles.forEach { file in
file.duplicates.forEach { key, path in
printPretty("error: Found duplicate key: \(key) in file: \(path)")
static func error(_ location: Locatable, _ error: String) {
print("\(location.location): error: \(error)")
}
static func warning(_ location: Locatable, _ warning: String) {
print("\(location.location): warning: \(warning)")
}
static func duplicate(
_ duplicate: KeyedLocatable,
original: KeyedLocatable
) {
print("\(duplicate.location): error: duplicate key '\(original.key)'")
// Looks like the `note:` doesn't work the same as when XCode does it unfortunately so we can't
// currently include the reference to the original entry
// print("\(original.location): note: previously found here")
}
}
// MARK: - ProjectState
struct ProjectState {
let primaryLocalizationFile: LocalizationStringsFile
let localizationFiles: [LocalizationStringsFile]
let sourceFiles: [SourceFile]
init(path: String, loadSourceFiles: Bool) {
guard
let enumerator: FileManager.DirectoryEnumerator = FileManager.default.enumerator(
at: URL(fileURLWithPath: path),
includingPropertiesForKeys: [.isDirectoryKey],
options: [.skipsHiddenFiles]
),
let fileUrls: [URL] = enumerator.allObjects as? [URL]
else { fatalError("Could not locate files in path directory: \(path)") }
// Get a list of valid URLs
let lowerCaseExcludedPaths: Set<String> = Set(ProjectState.excludedPaths.map { $0.lowercased() })
let validFileUrls: [URL] = fileUrls.filter { fileUrl in
((try? fileUrl.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == false) &&
!lowerCaseExcludedPaths.contains { fileUrl.path.lowercased().contains($0) }
}
// Localization files
let targetFileSuffixes: Set<String> = Set(ProjectState.validLocalisationSuffixes.map { $0.lowercased() })
self.localizationFiles = validFileUrls
.filter { fileUrl in targetFileSuffixes.contains { fileUrl.path.lowercased().contains($0) } }
.map { LocalizationStringsFile(path: $0.path) }
guard let primaryLocalizationFile: LocalizationStringsFile = self.localizationFiles.first(where: { $0.name == ProjectState.primaryLocalisationFile }) else {
fatalError("Could not locate primary localization file: \(ProjectState.primaryLocalisationFile)")
}
self.primaryLocalizationFile = primaryLocalizationFile
guard loadSourceFiles else {
self.sourceFiles = []
return
}
// Source files
let lowerCaseSourceSuffixes: Set<String> = Set(ProjectState.validSourceSuffixes.map { $0.lowercased() })
self.sourceFiles = validFileUrls
.filter { fileUrl in lowerCaseSourceSuffixes.contains(".\(fileUrl.pathExtension)") }
.compactMap { SourceFile(path: $0.path) }
}
}
protocol Locatable {
var location: String { get }
}
protocol KeyedLocatable: Locatable {
var key: String { get }
}
extension ProjectState {
// MARK: - LocalizationStringsFile
struct LocalizationStringsFile: Locatable {
struct Phrase: KeyedLocatable {
let key: String
let value: String
let filePath: String
let lineNumber: Int
var location: String { "\(filePath):\(lineNumber)" }
}
let name: String
let path: String
let keyPhrase: [String: Phrase]
let duplicates: [(Phrase, original: Phrase)]
var location: String { path }
init(path: String) {
let result = LocalizationStringsFile.parse(path)
self.name = (path
.replacingOccurrences(of: "/Localizable.strings", with: "")
.replacingOccurrences(of: ".lproj", with: "")
.components(separatedBy: "/")
.last ?? "Unknown")
self.path = path
self.keyPhrase = result.keyPhrase
self.duplicates = result.duplicates
}
static func parse(_ path: String) -> (keyPhrase: [String: Phrase], duplicates: [(Phrase, original: Phrase)]) {
guard
let data: Data = FileManager.default.contents(atPath: path),
let content: String = String(data: data, encoding: .utf8)
else { fatalError("Could not read from path: \(path)") }
let lines: [String] = content.components(separatedBy: .newlines)
var duplicates: [(Phrase, original: Phrase)] = []
var keyPhrase: [String: Phrase] = [:]
lines.enumerated().forEach { lineNumber, line in
guard
let key: String = Regex.matches("\"([^\"]*?)\"(?= =)", content: line).first,
let value: String = Regex.matches("(?<== )\"(.*?)\"(?=;)", content: line).first
else { return }
// Remove the quotation marks around the key
let trimmedKey: String = String(key
.prefix(upTo: key.index(before: key.endIndex))
.suffix(from: key.index(after: key.startIndex)))
// Files are 1-indexed but arrays are 0-indexed so add 1 to the lineNumber
let result: Phrase = Phrase(
key: trimmedKey,
value: value,
filePath: path,
lineNumber: (lineNumber + 1)
)
switch keyPhrase[trimmedKey] {
case .some(let original): duplicates.append((result, original))
case .none: keyPhrase[trimmedKey] = result
}
}
return (keyPhrase, duplicates)
}
}
validateMatchKeys(stringFiles)
// Note: Uncomment the below file to clean out all comments from the localizable file (we don't want this because comments make it readable...)
// stringFiles.forEach { $0.cleanWrite() }
let codeFiles: [LocalizationCodeFile] = localizedStringsInCode()
validateMissingKeys(codeFiles, localizationFiles: stringFiles)
validateDeadKeys(codeFiles, localizationFiles: stringFiles)
// MARK: - SourceFile
struct SourceFile: Locatable {
struct Phrase: KeyedLocatable {
let term: String
let filePath: String
let lineNumber: Int
var key: String { term }
var location: String { "\(filePath):\(lineNumber)" }
}
let path: String
let keyPhrase: [String: Phrase]
let unlocalizedKeyPhrase: [String: Phrase]
let phrases: [Phrase]
let unlocalizedPhrases: [Phrase]
var location: String { path }
init?(path: String) {
guard let result = SourceFile.parse(path) else { return nil }
self.path = path
self.keyPhrase = result.keyPhrase
self.unlocalizedKeyPhrase = result.unlocalizedKeyPhrase
self.phrases = result.phrases
self.unlocalizedPhrases = result.unlocalizedPhrases
}
static func parse(_ path: String) -> (keyPhrase: [String: Phrase], phrases: [Phrase], unlocalizedKeyPhrase: [String: Phrase], unlocalizedPhrases: [Phrase])? {
guard
let data: Data = FileManager.default.contents(atPath: path),
let content: String = String(data: data, encoding: .utf8)
else { fatalError("Could not read from path: \(path)") }
// If the file has the lint supression before the first import then ignore the file
let preImportContent: String = (content.components(separatedBy: "import").first ?? "")
guard !preImportContent.contains(ProjectState.lintSuppression) else {
print("Explicitly ignoring \(path)")
return nil
}
// Otherwise continue and process the file
let lines: [String] = content.components(separatedBy: .newlines)
var keyPhrase: [String: Phrase] = [:]
var unlocalizedKeyPhrase: [String: Phrase] = [:]
var phrases: [Phrase] = []
var unlocalizedPhrases: [Phrase] = []
lines.enumerated().forEach { lineNumber, line in
let trimmedLine: String = line.trimmingCharacters(in: .whitespacesAndNewlines)
// Ignore the line if it doesn't contain a quotation character (optimisation), it's
// been suppressed or it's explicitly excluded due to the rules at the top of the file
guard
trimmedLine.contains("\"") &&
!ProjectState.excludedUnlocalisedStringLineMatching
.contains(where: { $0.matches(trimmedLine, lineNumber, lines) })
else { return }
// Split line based on commented out content and exclude the comment from the linting
let commentMatches: [String] = Regex.matches(
"//[^\\\"]*(?:\\\"[^\\\"]*\\\"[^\\\"]*)*",
content: line
)
let targetLine: String = (commentMatches.isEmpty ? line :
line.components(separatedBy: commentMatches[0])[0]
)
// Use regex to find `NSLocalizedString("", "")`, `"".localised()` and any other `""`
// values in the source code
//
// Note: It's more complex because we need to exclude escaped quotation marks from
// strings and also want to ignore any strings that have been commented out, Swift
// also doesn't support "lookbehind" in regex so we can use that approach
var isUnlocalized: Bool = false
var allMatches: Set<String> = Set(
Regex
.matches(
"NSLocalizedString\\(@{0,1}\\\"[^\\\"\\\\]*(?:\\\\.[^\\\"\\\\]*)*(?:\\\")",
content: targetLine
)
.map { match in
match
.removingPrefixIfPresent("NSLocalizedString(@\"")
.removingPrefixIfPresent("NSLocalizedString(\"")
.removingSuffixIfPresent("\")")
.removingSuffixIfPresent("\"")
}
)
// If we didn't get any matches for the standard `NSLocalizedString` then try our
// custom extension `"".localized()`
if allMatches.isEmpty {
allMatches = allMatches.union(Set(
Regex
.matches(
"\\\"[^\\\"\\\\]*(?:\\\\.[^\\\"\\\\]*)*\\\"\\.localized",
content: targetLine
)
.map { match in
match
.removingPrefixIfPresent("\"")
.removingSuffixIfPresent("\".localized")
}
))
}
/// If we still don't have any matches then try to match any strings as unlocalized strings (handling
/// nested `"Test\"string\" value"`, empty strings and strings only composed of quotes `"""""""`)
///
/// **Note:** While it'd be nice to have the regex automatically exclude the quotes doing so makes it _far_ less
/// efficient (approx. by a factor of 8 times) so we remove those ourselves)
if allMatches.isEmpty {
// Find strings which are just not localised
let potentialUnlocalizedStrings: [String] = Regex
.matches("\\\"[^\\\"\\\\]*(?:\\\\.[^\\\"\\\\]*)*(?:\\\")", content: targetLine)
// Remove the leading and trailing quotation marks
.map { $0.removingPrefixIfPresent("\"").removingSuffixIfPresent("\"") }
// Remove any empty strings
.filter { !$0.isEmpty }
// Remove any string conversations (ie. `.map { "\($0)" }`
.filter { value in !value.hasPrefix("\\(") || !value.hasSuffix(")") }
allMatches = allMatches.union(Set(potentialUnlocalizedStrings))
isUnlocalized = true
}
// Remove any excluded phrases from the matches
allMatches = allMatches.subtracting(ProjectState.excludedPhrases.map { "\($0)" })
allMatches.forEach { match in
// Files are 1-indexed but arrays are 0-indexed so add 1 to the lineNumber
let result: Phrase = Phrase(
term: match,
filePath: path,
lineNumber: (lineNumber + 1)
)
if !isUnlocalized {
keyPhrase[match] = result
phrases.append(result)
}
else {
unlocalizedKeyPhrase[match] = result
unlocalizedPhrases.append(result)
}
}
}
return (keyPhrase, phrases, unlocalizedKeyPhrase, unlocalizedPhrases)
}
}
}
print("------------ Complete ------------")
indirect enum MatchType: Hashable {
case prefix(String)
case contains(String)
case containsAnd(String, MatchType)
case regex(String)
case previousLine(numEarlier: Int, MatchType)
func matches(_ value: String, _ index: Int, _ lines: [String]) -> Bool {
switch self {
case .prefix(let prefix):
return value
.trimmingCharacters(in: .whitespacesAndNewlines)
.hasPrefix(prefix)
case .contains(let other): return value.contains(other)
case .containsAnd(let other, let otherMatch):
guard value.contains(other) else { return false }
return otherMatch.matches(value, index, lines)
case .regex(let regex): return !Regex.matches(regex, content: value).isEmpty
case .previousLine(let numEarlier, let type):
guard index >= numEarlier else { return false }
let targetIndex: Int = (index - numEarlier)
return type.matches(lines[targetIndex], targetIndex, lines)
}
}
}
extension String {
func removingPrefixIfPresent(_ value: String) -> String {
guard hasPrefix(value) else { return self }
return String(self.suffix(from: self.index(self.startIndex, offsetBy: value.count)))
}
func removingSuffixIfPresent(_ value: String) -> String {
guard hasSuffix(value) else { return self }
return String(self.prefix(upTo: self.index(self.endIndex, offsetBy: -value.count)))
}
}

View File

@ -802,6 +802,11 @@
FDC438C927BB706500C60D73 /* SendDirectMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */; };
FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */; };
FDC438CD27BC641200C60D73 /* Set+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CC27BC641200C60D73 /* Set+Utilities.swift */; };
FDC498B72AC15F7D00EDD897 /* AppNotificationCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC498B62AC15F7D00EDD897 /* AppNotificationCategory.swift */; };
FDC498B92AC15FE300EDD897 /* AppNotificationAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC498B82AC15FE300EDD897 /* AppNotificationAction.swift */; };
FDC498BB2AC1606C00EDD897 /* AppNotificationUserInfoKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC498BA2AC1606C00EDD897 /* AppNotificationUserInfoKey.swift */; };
FDC498BE2AC1732E00EDD897 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = FD9401A32ABD04AC003A4834 /* Localizable.strings */; };
FDC498C22AC17BFC00EDD897 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = FD9401A32ABD04AC003A4834 /* Localizable.strings */; };
FDC6D6F32860607300B04575 /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7542807C4BB004C14C5 /* Environment.swift */; };
FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC6D75F2862B3F600B04575 /* Dependencies.swift */; };
FDCD2E032A41294E00964D6A /* LegacyGroupOnlyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */; };
@ -1940,6 +1945,9 @@
FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageRequest.swift; sourceTree = "<group>"; };
FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateMessageRequest.swift; sourceTree = "<group>"; };
FDC438CC27BC641200C60D73 /* Set+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Set+Utilities.swift"; sourceTree = "<group>"; };
FDC498B62AC15F7D00EDD897 /* AppNotificationCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNotificationCategory.swift; sourceTree = "<group>"; };
FDC498B82AC15FE300EDD897 /* AppNotificationAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNotificationAction.swift; sourceTree = "<group>"; };
FDC498BA2AC1606C00EDD897 /* AppNotificationUserInfoKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNotificationUserInfoKey.swift; sourceTree = "<group>"; };
FDC6D75F2862B3F600B04575 /* Dependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependencies.swift; sourceTree = "<group>"; };
FDCCC6E82ABA7402002BBEF5 /* EmojiGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiGenerator.swift; sourceTree = "<group>"; };
FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGroupOnlyRequest.swift; sourceTree = "<group>"; };
@ -3102,6 +3110,7 @@
C36096BB25AD1BBB008B62B2 /* Notifications */ = {
isa = PBXGroup;
children = (
FDC498B52AC15F6D00EDD897 /* Types */,
4539B5851F79348F007141FF /* PushRegistrationManager.swift */,
45CD81EE1DC030E7004C9430 /* SyncPushTokensJob.swift */,
451A13B01E13DED2000A50FD /* AppNotifications.swift */,
@ -4303,6 +4312,16 @@
path = _TestUtilities;
sourceTree = "<group>";
};
FDC498B52AC15F6D00EDD897 /* Types */ = {
isa = PBXGroup;
children = (
FDC498B62AC15F7D00EDD897 /* AppNotificationCategory.swift */,
FDC498B82AC15FE300EDD897 /* AppNotificationAction.swift */,
FDC498BA2AC1606C00EDD897 /* AppNotificationUserInfoKey.swift */,
);
path = Types;
sourceTree = "<group>";
};
FDDC08F029A300D500BF9681 /* Utilities */ = {
isa = PBXGroup;
children = (
@ -4527,6 +4546,7 @@
453518641FC635DD00210559 /* Sources */,
453518651FC635DD00210559 /* Frameworks */,
453518661FC635DD00210559 /* Resources */,
FDC498C02AC1774500EDD897 /* Ensure Localizable.strings included */,
);
buildRules = (
);
@ -4550,6 +4570,7 @@
7BC01A37241F40AB00BC7C55 /* Sources */,
7BC01A38241F40AB00BC7C55 /* Frameworks */,
7BC01A39241F40AB00BC7C55 /* Resources */,
FDC498C12AC1775400EDD897 /* Ensure Localizable.strings included */,
);
buildRules = (
);
@ -4672,6 +4693,7 @@
453518771FC635DD00210559 /* Embed Foundation Extensions */,
4535189F1FC63DBF00210559 /* Embed Frameworks */,
FDD82C422A2085B900425F05 /* Add Commit Hash To Build Info Plist */,
FDC498BF2AC1747900EDD897 /* Ensure Localizable.strings included */,
90DF4725BB1271EBA2C66A12 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
@ -4964,6 +4986,7 @@
buildActionMask = 2147483647;
files = (
4535186E1FC635DD00210559 /* MainInterface.storyboard in Resources */,
FDC498C22AC17BFC00EDD897 /* Localizable.strings in Resources */,
B8D07406265C683A00F77E07 /* ElegantIcons.ttf in Resources */,
3478504C1FD7496D007B8332 /* Images.xcassets in Resources */,
);
@ -4973,6 +4996,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FDC498BE2AC1732E00EDD897 /* Localizable.strings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -5429,6 +5453,66 @@
shellScript = "\"${SRCROOT}/Scripts/build_libSession_util.sh\"\n";
showEnvVarsInLog = 0;
};
FDC498BF2AC1747900EDD897 /* Ensure Localizable.strings included */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Ensure Localizable.strings included";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Scripts/LintLocalizableStrings.swift\" validate\n";
showEnvVarsInLog = 0;
};
FDC498C02AC1774500EDD897 /* Ensure Localizable.strings included */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Ensure Localizable.strings included";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Scripts/LintLocalizableStrings.swift\" validate\n";
showEnvVarsInLog = 0;
};
FDC498C12AC1775400EDD897 /* Ensure Localizable.strings included */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Ensure Localizable.strings included";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Scripts/LintLocalizableStrings.swift\" validate\n";
showEnvVarsInLog = 0;
};
FDD82C422A2085B900425F05 /* Add Commit Hash To Build Info Plist */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@ -5468,7 +5552,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Scripts/LintLocalizableStrings.swift\"\n";
shellScript = "\"${SRCROOT}/Scripts/LintLocalizableStrings.swift\" lint\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
@ -6083,6 +6167,7 @@
FD87DCFE28B7582C00AF0F98 /* BlockedContactsViewModel.swift in Sources */,
FD37E9DD28A384EB003AE748 /* PrimaryColorSelectionView.swift in Sources */,
B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */,
FDC498B72AC15F7D00EDD897 /* AppNotificationCategory.swift in Sources */,
FDFDE126282D05380098B17F /* MediaInteractiveDismiss.swift in Sources */,
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
C331FFFE2558FF3B00070591 /* FullConversationCell.swift in Sources */,
@ -6100,6 +6185,7 @@
7BBBDC462875600700747E59 /* DocumentTitleViewController.swift in Sources */,
FD71163F28E2C82C00B47552 /* SessionHeaderView.swift in Sources */,
B877E24226CA12910007970A /* CallVC.swift in Sources */,
FDC498B92AC15FE300EDD897 /* AppNotificationAction.swift in Sources */,
7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */,
C374EEEB25DA3CA70073A857 /* ConversationTitleView.swift in Sources */,
FD716E7128505E5200C96BF4 /* MessageRequestsCell.swift in Sources */,
@ -6161,6 +6247,7 @@
7B9F71D72853100A006DFE7B /* Emoji+Available.swift in Sources */,
FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */,
7B9F71D32852EEE2006DFE7B /* Emoji.swift in Sources */,
FDC498BB2AC1606C00EDD897 /* AppNotificationUserInfoKey.swift in Sources */,
C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */,
B82B4090239DD75000A248E7 /* RestoreVC.swift in Sources */,
3488F9362191CC4000E524CC /* MediaView.swift in Sources */,
@ -6507,7 +6594,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 425;
CURRENT_PROJECT_VERSION = 426;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6579,7 +6666,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 425;
CURRENT_PROJECT_VERSION = 426;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -6644,7 +6731,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 425;
CURRENT_PROJECT_VERSION = 426;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6718,7 +6805,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 425;
CURRENT_PROJECT_VERSION = 426;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -7678,7 +7765,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 425;
CURRENT_PROJECT_VERSION = 426;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -7749,7 +7836,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 425;
CURRENT_PROJECT_VERSION = 426;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",

View File

@ -1,213 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSBackupSettingsViewController.h"
#import "OWSBackup.h"
#import "Session-Swift.h"
#import <PromiseKit/AnyPromise.h>
#import <SessionMessagingKit/Environment.h>
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
#import <SignalUtilitiesKit/UIColor+OWS.h>
#import <SignalUtilitiesKit/UIFont+OWS.h>
#import <SessionUtilitiesKit/UIView+OWS.h>
#import <SessionUtilitiesKit/MIMETypeUtil.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWSBackupSettingsViewController ()
@property (nonatomic, nullable) NSError *iCloudError;
@end
#pragma mark -
@implementation OWSBackupSettingsViewController
#pragma mark - Dependencies
- (OWSBackup *)backup
{
OWSAssertDebug(AppEnvironment.shared.backup);
return AppEnvironment.shared.backup;
}
#pragma mark -
- (void)viewDidLoad
{
[super viewDidLoad];
self.title = NSLocalizedString(@"SETTINGS_BACKUP", @"Label for the backup view in app settings.");
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backupStateDidChange:)
name:NSNotificationNameBackupStateDidChange
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidBecomeActive:)
name:OWSApplicationDidBecomeActiveNotification
object:nil];
[self updateTableContents];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self updateTableContents];
[self updateICloudStatus];
}
- (void)updateICloudStatus
{
__weak OWSBackupSettingsViewController *weakSelf = self;
[[self.backup ensureCloudKitAccess]
.then(^{
OWSAssertIsOnMainThread();
weakSelf.iCloudError = nil;
[weakSelf updateTableContents];
})
.catch(^(NSError *error) {
OWSAssertIsOnMainThread();
weakSelf.iCloudError = error;
[weakSelf updateTableContents];
}) retainUntilComplete];
}
#pragma mark - Table Contents
- (void)updateTableContents
{
OWSTableContents *contents = [OWSTableContents new];
BOOL isBackupEnabled = [OWSBackup.sharedManager isBackupEnabled];
if (self.iCloudError) {
OWSTableSection *iCloudSection = [OWSTableSection new];
iCloudSection.headerTitle = NSLocalizedString(
@"SETTINGS_BACKUP_ICLOUD_STATUS", @"Label for iCloud status row in the in the backup settings view.");
[iCloudSection
addItem:[OWSTableItem
longDisclosureItemWithText:[OWSBackupAPI errorMessageForCloudKitAccessError:self.iCloudError]
actionBlock:^{
[[UIApplication sharedApplication]
openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
}]];
[contents addSection:iCloudSection];
}
// TODO: This UI is temporary.
// Enabling backup will involve entering and registering a PIN.
OWSTableSection *enableSection = [OWSTableSection new];
enableSection.headerTitle = NSLocalizedString(@"SETTINGS_BACKUP", @"Label for the backup view in app settings.");
[enableSection
addItem:[OWSTableItem switchItemWithText:
NSLocalizedString(@"SETTINGS_BACKUP_ENABLING_SWITCH",
@"Label for switch in settings that controls whether or not backup is enabled.")
isOnBlock:^{
return [OWSBackup.sharedManager isBackupEnabled];
}
target:self
selector:@selector(isBackupEnabledDidChange:)]];
[contents addSection:enableSection];
if (isBackupEnabled) {
// TODO: This UI is temporary.
// Enabling backup will involve entering and registering a PIN.
OWSTableSection *progressSection = [OWSTableSection new];
[progressSection
addItem:[OWSTableItem
labelItemWithText:NSLocalizedString(@"SETTINGS_BACKUP_STATUS",
@"Label for backup status row in the in the backup settings view.")
accessoryText:NSStringForBackupExportState(OWSBackup.sharedManager.backupExportState)]];
if (OWSBackup.sharedManager.backupExportState == OWSBackupState_InProgress) {
if (OWSBackup.sharedManager.backupExportDescription) {
[progressSection
addItem:[OWSTableItem
labelItemWithText:NSLocalizedString(@"SETTINGS_BACKUP_PHASE",
@"Label for phase row in the in the backup settings view.")
accessoryText:OWSBackup.sharedManager.backupExportDescription]];
if (OWSBackup.sharedManager.backupExportProgress) {
NSUInteger progressPercent
= (NSUInteger)round(OWSBackup.sharedManager.backupExportProgress.floatValue * 100);
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setNumberStyle:NSNumberFormatterPercentStyle];
[numberFormatter setMaximumFractionDigits:0];
[numberFormatter setMultiplier:@1];
NSString *progressString = [numberFormatter stringFromNumber:@(progressPercent)];
[progressSection
addItem:[OWSTableItem
labelItemWithText:NSLocalizedString(@"SETTINGS_BACKUP_PROGRESS",
@"Label for phase row in the in the backup settings view.")
accessoryText:progressString]];
}
}
}
switch (OWSBackup.sharedManager.backupExportState) {
case OWSBackupState_Idle:
case OWSBackupState_Failed:
case OWSBackupState_Succeeded:
[progressSection
addItem:[OWSTableItem disclosureItemWithText:
NSLocalizedString(@"SETTINGS_BACKUP_BACKUP_NOW",
@"Label for 'backup now' button in the backup settings view.")
actionBlock:^{
[OWSBackup.sharedManager tryToExportBackup];
}]];
break;
case OWSBackupState_InProgress:
[progressSection
addItem:[OWSTableItem disclosureItemWithText:
NSLocalizedString(@"SETTINGS_BACKUP_CANCEL_BACKUP",
@"Label for 'cancel backup' button in the backup settings view.")
actionBlock:^{
[OWSBackup.sharedManager cancelExportBackup];
}]];
break;
}
[contents addSection:progressSection];
}
self.contents = contents;
}
- (void)isBackupEnabledDidChange:(UISwitch *)sender
{
[OWSBackup.sharedManager setIsBackupEnabled:sender.isOn];
[self updateTableContents];
}
#pragma mark - Events
- (void)backupStateDidChange:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
[self updateTableContents];
}
- (void)applicationDidBecomeActive:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
[self updateICloudStatus];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,6 +1,9 @@
// This file is generated by EmojiGenerator.swift, do not manually edit it.
// swiftlint:disable all
// stringlint:disable
extension Emoji {
enum Category: String, CaseIterable, Equatable {
case smileysAndPeople = "Smileys & People"
@ -3816,3 +3819,4 @@ extension Emoji {
}
}
}
// swiftlint:disable all

View File

@ -1,6 +1,9 @@
// This file is generated by EmojiGenerator.swift, do not manually edit it.
// swiftlint:disable all
// stringlint:disable
extension Emoji {
var name: String {
switch self {
@ -1882,3 +1885,4 @@ extension Emoji {
}
}
}
// swiftlint:disable all

View File

@ -1,6 +1,9 @@
// This file is generated by EmojiGenerator.swift, do not manually edit it.
// swiftlint:disable all
// stringlint:disable
extension Emoji {
enum SkinTone: String, CaseIterable, Equatable {
case light = "🏻"
@ -2738,3 +2741,4 @@ extension Emoji {
}
}
}
// swiftlint:disable all

View File

@ -2,6 +2,7 @@
// This file is generated by EmojiGenerator.swift, do not manually edit it.
// swiftlint:disable all
// stringlint:disable
/// A sorted representation of all available emoji
enum Emoji: String, CaseIterable, Equatable {

View File

@ -1,6 +1,9 @@
// This file is generated by EmojiGenerator.swift, do not manually edit it.
// swiftlint:disable all
// stringlint:disable
extension EmojiWithSkinTones {
init?(rawValue: String) {
guard rawValue.isSingleEmoji else { return nil }
@ -4726,3 +4729,4 @@ extension EmojiWithSkinTones {
return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue))
}
}
// swiftlint:disable all

View File

@ -64,7 +64,7 @@ extension Emoji {
guard withDefaultEmoji else { return recentReactionEmoji }
// Add in our default emoji if desired
let defaultEmoji = ["😂", "🥰", "😢", "😡", "😮", "😈"]
let defaultEmoji = ["😂", "🥰", "😢", "😡", "😮", "😈"] // stringlint:disable
.filter { !recentReactionEmoji.contains($0) }
return Array(recentReactionEmoji

View File

@ -56,32 +56,26 @@ class GiphyRendition: ProxiedContentAssetDescription {
private class func fileExtension(forFormat format: GiphyFormat) -> String {
switch format {
case .gif:
return "gif"
case .mp4:
return "mp4"
case .jpg:
return "jpg"
case .gif: return "gif" // stringlint:disable
case .mp4: return "mp4" // stringlint:disable
case .jpg: return "jpg" // stringlint:disable
}
}
public var utiType: String {
switch format {
case .gif:
return kUTTypeGIF as String
case .mp4:
return kUTTypeMPEG4 as String
case .jpg:
return kUTTypeJPEG as String
case .gif: return kUTTypeGIF as String
case .mp4: return kUTTypeMPEG4 as String
case .jpg: return kUTTypeJPEG as String
}
}
public var isStill: Bool {
return name.hasSuffix("_still")
return name.hasSuffix("_still") // stringlint:disable
}
public var isDownsampled: Bool {
return name.hasSuffix("_downsampled")
return name.hasSuffix("_downsampled") // stringlint:disable
}
public func log() {
@ -279,11 +273,11 @@ enum GiphyAPI {
// MARK: - Search
// This is the Signal iOS API key.
private static let kGiphyApiKey = "ZsUpUm2L6cVbvei347EQNp7HrROjbOdc"
private static let kGiphyApiKey = "ZsUpUm2L6cVbvei347EQNp7HrROjbOdc" // stringlint:disable
private static let kGiphyPageSize = 20
public static func trending() -> AnyPublisher<[GiphyImageInfo], Error> {
let urlString = "/v1/gifs/trending?api_key=\(kGiphyApiKey)&limit=\(kGiphyPageSize)"
let urlString = "/v1/gifs/trending?api_key=\(kGiphyApiKey)&limit=\(kGiphyPageSize)" // stringlint:disable
guard let url: URL = URL(string: "\(kGiphyBaseURL)\(urlString)") else {
return Fail(error: HTTPError.invalidURL)
@ -319,10 +313,10 @@ enum GiphyAPI {
let url: URL = URL(
string: [
kGiphyBaseURL,
"/v1/gifs/search?api_key=\(kGiphyApiKey)",
"&offset=\(kGiphyPageOffset)",
"&limit=\(kGiphyPageSize)",
"&q=\(queryEncoded)"
"/v1/gifs/search?api_key=\(kGiphyApiKey)", // stringlint:disable
"&offset=\(kGiphyPageOffset)", // stringlint:disable
"&limit=\(kGiphyPageSize)", // stringlint:disable
"&q=\(queryEncoded)" // stringlint:disable
].joined()
)
else {
@ -370,7 +364,7 @@ enum GiphyAPI {
Logger.error("Invalid response.")
return nil
}
guard let imageDicts = responseDict["data"] as? [[String: Any]] else {
guard let imageDicts = responseDict["data"] as? [[String: Any]] else { // stringlint:disable
Logger.error("Invalid response data.")
return nil
}
@ -381,7 +375,7 @@ enum GiphyAPI {
// Giphy API results are often incomplete or malformed, so we need to be defensive.
private static func parseGiphyImage(imageDict: [String: Any]) -> GiphyImageInfo? {
guard let giphyId = imageDict["id"] as? String else {
guard let giphyId = imageDict["id"] as? String else { // stringlint:disable
Logger.warn("Image dict missing id.")
return nil
}
@ -389,7 +383,7 @@ enum GiphyAPI {
Logger.warn("Image dict has invalid id.")
return nil
}
guard let renditionDicts = imageDict["images"] as? [String: Any] else {
guard let renditionDicts = imageDict["images"] as? [String: Any] else { // stringlint:disable
Logger.warn("Image dict missing renditions.")
return nil
}
@ -423,7 +417,7 @@ enum GiphyAPI {
}
private static func findOriginalRendition(renditions: [GiphyRendition]) -> GiphyRendition? {
for rendition in renditions where rendition.name == "original" {
for rendition in renditions where rendition.name == "original" { // stringlint:disable
return rendition
}
return nil
@ -436,15 +430,15 @@ enum GiphyAPI {
renditionName: String,
renditionDict: [String: Any]
) -> GiphyRendition? {
guard let width = parsePositiveUInt(dict: renditionDict, key: "width", typeName: "rendition") else {
guard let width = parsePositiveUInt(dict: renditionDict, key: "width", typeName: "rendition") else { // stringlint:disable
return nil
}
guard let height = parsePositiveUInt(dict: renditionDict, key: "height", typeName: "rendition") else {
guard let height = parsePositiveUInt(dict: renditionDict, key: "height", typeName: "rendition") else { // stringlint:disable
return nil
}
// Be lenient when parsing file sizes - we don't require them for stills.
let fileSize = parseLenientUInt(dict: renditionDict, key: "size")
guard let urlString = renditionDict["url"] as? String else {
let fileSize = parseLenientUInt(dict: renditionDict, key: "size") // stringlint:disable
guard let urlString = renditionDict["url"] as? String else { // stringlint:disable
return nil
}
guard urlString.count > 0 else {
@ -460,13 +454,13 @@ enum GiphyAPI {
return nil
}
var format = GiphyFormat.gif
if fileExtension == "gif" {
if fileExtension == "gif" { // stringlint:disable
format = .gif
} else if fileExtension == "jpg" {
} else if fileExtension == "jpg" { // stringlint:disable
format = .jpg
} else if fileExtension == "mp4" {
} else if fileExtension == "mp4" { // stringlint:disable
format = .mp4
} else if fileExtension == "webp" {
} else if fileExtension == "webp" { // stringlint:disable
return nil
} else {
Logger.warn("Invalid file extension: \(fileExtension).")

View File

@ -145,7 +145,7 @@ public class MediaGalleryViewModel {
public struct GalleryDate: Differentiable, Equatable, Comparable, Hashable {
private static let thisYearFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM"
formatter.dateFormat = "MMMM" // stringlint:disable
return formatter
}()
@ -153,7 +153,7 @@ public class MediaGalleryViewModel {
private static let olderFormatter: DateFormatter = {
// FIXME: localize for RTL, or is there a built in way to do this?
let formatter = DateFormatter()
formatter.dateFormat = "MMMM yyyy"
formatter.dateFormat = "MMMM yyyy" // stringlint:disable
return formatter
}()

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import Combine

View File

@ -93,8 +93,7 @@
/* Label indicating loading is in progress */
"GALLERY_TILES_LOADING_OLDER_LABEL" = "Зареждане на по-стари файлове…";
/* Error displayed when there is a failure fetching a GIF from the remote service. */
"GIF_PICKER_ERROR_FETCH_FAILURE" = "Извличането на посочения GIF е неуспешно.
Моля, проверете интернет връзката си.";
"GIF_PICKER_ERROR_FETCH_FAILURE" = "Извличането на посочения GIF е неуспешно. Моля, проверете интернет връзката си.";
/* Generic error displayed when picking a GIF */
"GIF_PICKER_ERROR_GENERIC" = "Възникна неизвестна грешка.";
/* Shown when selected GIF couldn't be fetched */

View File

@ -486,8 +486,7 @@
"PRIVACY_CALLS_TITLE" = "Llamadas de voz y video";
"PRIVACY_CALLS_DESCRIPTION" = "Habilita llamadas de voz y video hacia y desde otros usuarios.";
"PRIVACY_CALLS_WARNING_TITLE" = "Llamadas de Voz y Video (Beta)";
"PRIVACY_CALLS_WARNING_DESCRIPTION" = "Su dirección IP es visible para su socio de llamadas y un servidor de Oxen Fundación
mientras usas llamadas beta. ¿Está seguro de que desea habilitar las llamadas de voz y videollamadas?";
"PRIVACY_CALLS_WARNING_DESCRIPTION" = "Su dirección IP es visible para su socio de llamadas y un servidor de Oxen Fundación mientras usas llamadas beta. ¿Está seguro de que desea habilitar las llamadas de voz y videollamadas?";
"NOTIFICATIONS_TITLE" = "Notificaciones";
"NOTIFICATIONS_SECTION_STRATEGY" = "Estrategia de notificación";
"NOTIFICATIONS_STRATEGY_FAST_MODE_TITLE" = "Usar Modo Rápido";

View File

@ -24,68 +24,6 @@ import SessionSnodeKit
/// there is no need for an Adapter, and instead the appropriate NotificationActionHandler is
/// wired directly into the appropriate callback point.
enum AppNotificationCategory: CaseIterable {
case incomingMessage
case incomingMessageFromNoLongerVerifiedIdentity
case errorMessage
case threadlessErrorMessage
}
enum AppNotificationAction: CaseIterable {
case markAsRead
case reply
case showThread
}
struct AppNotificationUserInfoKey {
static let threadId = "Signal.AppNotificationsUserInfoKey.threadId"
static let threadVariantRaw = "Signal.AppNotificationsUserInfoKey.threadVariantRaw"
static let callBackNumber = "Signal.AppNotificationsUserInfoKey.callBackNumber"
static let localCallId = "Signal.AppNotificationsUserInfoKey.localCallId"
static let threadNotificationCounter = "Session.AppNotificationsUserInfoKey.threadNotificationCounter"
}
extension AppNotificationCategory {
var identifier: String {
switch self {
case .incomingMessage:
return "Signal.AppNotificationCategory.incomingMessage"
case .incomingMessageFromNoLongerVerifiedIdentity:
return "Signal.AppNotificationCategory.incomingMessageFromNoLongerVerifiedIdentity"
case .errorMessage:
return "Signal.AppNotificationCategory.errorMessage"
case .threadlessErrorMessage:
return "Signal.AppNotificationCategory.threadlessErrorMessage"
}
}
var actions: [AppNotificationAction] {
switch self {
case .incomingMessage:
return [.markAsRead, .reply]
case .incomingMessageFromNoLongerVerifiedIdentity:
return [.markAsRead, .showThread]
case .errorMessage:
return [.showThread]
case .threadlessErrorMessage:
return []
}
}
}
extension AppNotificationAction {
var identifier: String {
switch self {
case .markAsRead:
return "Signal.AppNotifications.Action.markAsRead"
case .reply:
return "Signal.AppNotifications.Action.reply"
case .showThread:
return "Signal.AppNotifications.Action.showThread"
}
}
}
let kAudioNotificationsThrottleCount = 2
let kAudioNotificationsThrottleInterval: TimeInterval = 5

View File

@ -183,6 +183,6 @@ private func redact(_ string: String) -> String {
#if DEBUG
return string
#else
return "[ READACTED \(string.prefix(2))...\(string.suffix(2)) ]"
return "[ READACTED \(string.prefix(2))...\(string.suffix(2)) ]" // stringlint:disable
#endif
}

View File

@ -0,0 +1,21 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
enum AppNotificationAction: CaseIterable {
case markAsRead
case reply
case showThread
}
extension AppNotificationAction {
var identifier: String {
switch self {
case .markAsRead: return "Signal.AppNotifications.Action.markAsRead"
case .reply: return "Signal.AppNotifications.Action.reply"
case .showThread: return "Signal.AppNotifications.Action.showThread"
}
}
}

View File

@ -0,0 +1,34 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
enum AppNotificationCategory: CaseIterable {
case incomingMessage
case incomingMessageFromNoLongerVerifiedIdentity
case errorMessage
case threadlessErrorMessage
}
extension AppNotificationCategory {
var identifier: String {
switch self {
case .incomingMessage: return "Signal.AppNotificationCategory.incomingMessage"
case .incomingMessageFromNoLongerVerifiedIdentity:
return "Signal.AppNotificationCategory.incomingMessageFromNoLongerVerifiedIdentity"
case .errorMessage: return "Signal.AppNotificationCategory.errorMessage"
case .threadlessErrorMessage: return "Signal.AppNotificationCategory.threadlessErrorMessage"
}
}
var actions: [AppNotificationAction] {
switch self {
case .incomingMessage: return [.markAsRead, .reply]
case .incomingMessageFromNoLongerVerifiedIdentity: return [.markAsRead, .showThread]
case .errorMessage: return [.showThread]
case .threadlessErrorMessage: return []
}
}
}

View File

@ -0,0 +1,13 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
struct AppNotificationUserInfoKey {
static let threadId = "Signal.AppNotificationsUserInfoKey.threadId"
static let threadVariantRaw = "Signal.AppNotificationsUserInfoKey.threadVariantRaw"
static let callBackNumber = "Signal.AppNotificationsUserInfoKey.callBackNumber"
static let localCallId = "Signal.AppNotificationsUserInfoKey.localCallId"
static let threadNotificationCounter = "Session.AppNotificationsUserInfoKey.threadNotificationCounter"
}

View File

@ -191,7 +191,7 @@ final class RegisterVC : BaseVC {
for index in indexesToShuffle {
let startIndex = mangledHexEncodedPublicKey.index(mangledHexEncodedPublicKey.startIndex, offsetBy: index)
let endIndex = mangledHexEncodedPublicKey.index(after: startIndex)
mangledHexEncodedPublicKey.replaceSubrange(startIndex..<endIndex, with: "0123456789abcdef__".shuffled()[0..<1])
mangledHexEncodedPublicKey.replaceSubrange(startIndex..<endIndex, with: "0123456789abcdef__".shuffled()[0..<1]) // stringlint:disable
}
count += 1
if count < limit {

View File

@ -18,12 +18,12 @@ final class SeedVC: BaseVC {
let hasStoredPublicKey: Bool = (Identity.fetchUserPublicKey() != nil)
let hasStoredEdKeyPair: Bool = (Identity.fetchUserEd25519KeyPair() != nil)
let dbStates: [String] = [
"dbIsValid: \(dbIsValid)",
"dbIsSuspendedUnsafe: \(dbIsSuspendedUnsafe)",
"storedSeed: false",
"userPublicKey: \(hasStoredPublicKey)",
"userPrivateKey: false",
"userEdKeyPair: \(hasStoredEdKeyPair)"
"dbIsValid: \(dbIsValid)", // stringlint:disable
"dbIsSuspendedUnsafe: \(dbIsSuspendedUnsafe)", // stringlint:disable
"storedSeed: false", // stringlint:disable
"userPublicKey: \(hasStoredPublicKey)", // stringlint:disable
"userPrivateKey: false", // stringlint:disable
"userEdKeyPair: \(hasStoredEdKeyPair)" // stringlint:disable
]
SNLog("Failed to retrieve keys for mnemonic generation (\(dbStates.joined(separator: ", ")))")

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import GRDB

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import Sodium

View File

@ -6,7 +6,7 @@ import SessionUtilitiesKit
enum _001_InitialSetupMigration: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "initialSetup"
static let identifier: String = "initialSetup" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1
@ -65,7 +65,7 @@ enum _001_InitialSetupMigration: Migration {
t.column(.variant, .integer).notNull()
t.column(.creationDateTimestamp, .double).notNull()
t.column(.shouldBeVisible, .boolean).notNull()
t.deprecatedColumn(name: "isPinned", .boolean).notNull()
t.deprecatedColumn(name: "isPinned", .boolean).notNull() // stringlint:disable
t.column(.messageDraft, .text)
t.column(.notificationSound, .integer)
t.column(.mutedUntilTimestamp, .double)

View File

@ -9,7 +9,7 @@ import SessionSnodeKit
/// before running the `YDBToGRDBMigration`
enum _002_SetupStandardJobs: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "SetupStandardJobs"
static let identifier: String = "SetupStandardJobs" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import AVKit

View File

@ -8,7 +8,7 @@ import SessionSnodeKit
/// This migration removes the legacy YapDatabase files
enum _004_RemoveLegacyYDB: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "RemoveLegacyYDB"
static let identifier: String = "RemoveLegacyYDB" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1

View File

@ -7,7 +7,7 @@ import SessionUtilitiesKit
/// This migration fixes a bug where certain message variants could incorrectly be counted as unread messages
enum _005_FixDeletedMessageReadState: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "FixDeletedMessageReadState"
static let identifier: String = "FixDeletedMessageReadState" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.01

View File

@ -8,7 +8,7 @@ import SessionUtilitiesKit
/// for open groups so they will fully re-fetch their mod/admin lists
enum _006_FixHiddenModAdminSupport: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "FixHiddenModAdminSupport"
static let identifier: String = "FixHiddenModAdminSupport" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.01

View File

@ -7,13 +7,13 @@ import SessionUtilitiesKit
/// This migration adds an index to the interaction table in order to improve the performance of retrieving the number of unread interactions
enum _007_HomeQueryOptimisationIndexes: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "HomeQueryOptimisationIndexes"
static let identifier: String = "HomeQueryOptimisationIndexes" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.01
static func migrate(_ db: Database) throws {
try db.create(
index: "interaction_on_wasRead_and_hasMention_and_threadId",
index: "interaction_on_wasRead_and_hasMention_and_threadId", // stringlint:disable
on: Interaction.databaseTableName,
columns: [
Interaction.Columns.wasRead.name,
@ -23,7 +23,7 @@ enum _007_HomeQueryOptimisationIndexes: Migration {
)
try db.create(
index: "interaction_on_threadId_and_timestampMs_and_variant",
index: "interaction_on_threadId_and_timestampMs_and_variant", // stringlint:disable
on: Interaction.databaseTableName,
columns: [
Interaction.Columns.threadId.name,

View File

@ -7,7 +7,7 @@ import SessionUtilitiesKit
/// This migration adds the new types needed for Emoji Reacts
enum _008_EmojiReacts: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "EmojiReacts"
static let identifier: String = "EmojiReacts" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.01

View File

@ -6,7 +6,7 @@ import SessionUtilitiesKit
enum _009_OpenGroupPermission: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "OpenGroupPermission"
static let identifier: String = "OpenGroupPermission" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.01

View File

@ -8,7 +8,7 @@ import SessionUtilitiesKit
/// searh (currently it's much slower than the global search)
enum _010_AddThreadIdToFTS: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "AddThreadIdToFTS"
static let identifier: String = "AddThreadIdToFTS" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 3

View File

@ -8,7 +8,7 @@ import SessionUtilitiesKit
/// message due to how one-to-one conversations work, by storing pending read receipts we should be able to prevent this case)
enum _011_AddPendingReadReceipts: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "AddPendingReadReceipts"
static let identifier: String = "AddPendingReadReceipts" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.01

View File

@ -7,7 +7,7 @@ import SessionUtilitiesKit
/// This migration adds the FTS table back for internal test users whose FTS table was removed unintentionally
enum _012_AddFTSIfNeeded: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "AddFTSIfNeeded"
static let identifier: String = "AddFTSIfNeeded" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.01

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import CryptoKit

View File

@ -8,7 +8,7 @@ import SessionUtilitiesKit
/// This migration goes through the current state of the database and generates config dumps for the user config types
enum _014_GenerateInitialUserConfigDumps: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "GenerateInitialUserConfigDumps"
static let identifier: String = "GenerateInitialUserConfigDumps" // stringlint:disable
static let needsConfigSync: Bool = true
static let minExpectedRunDuration: TimeInterval = 4.0

View File

@ -7,7 +7,7 @@ import SessionUtilitiesKit
/// This migration adds a flag indicating whether a profile has indicated it is blocking community message requests
enum _015_BlockCommunityMessageRequests: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "BlockCommunityMessageRequests"
static let identifier: String = "BlockCommunityMessageRequests" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.01
static var requirements: [MigrationRequirement] = [.sessionUtilStateLoaded]

View File

@ -8,7 +8,7 @@ import SessionUtilitiesKit
/// results in migration issues when a user jumps between multiple versions)
enum _016_MakeBrokenProfileTimestampsNullable: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "MakeBrokenProfileTimestampsNullable"
static let identifier: String = "MakeBrokenProfileTimestampsNullable" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1
static var requirements: [MigrationRequirement] = [.sessionUtilStateLoaded]
@ -17,7 +17,7 @@ enum _016_MakeBrokenProfileTimestampsNullable: Migration {
/// SQLite doesn't support altering columns after creation so we need to create a new table with the setup we
/// want, copy data from the old table over, drop the old table and rename the new table
struct TmpProfile: Codable, TableRecord, FetchableRecord, PersistableRecord, ColumnExpressible {
static var databaseTableName: String { "tmpProfile" }
static var databaseTableName: String { "tmpProfile" } // stringlint:disable
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {

View File

@ -284,19 +284,19 @@ extension Attachment: CustomStringConvertible {
public static func emoji(for contentType: String) -> String {
if MIMETypeUtil.isImage(contentType) {
return "📷"
return "📷" // stringlint:disable
}
else if MIMETypeUtil.isVideo(contentType) {
return "🎥"
return "🎥" // stringlint:disable
}
else if MIMETypeUtil.isAudio(contentType) {
return "🎧"
return "🎧" // stringlint:disable
}
else if MIMETypeUtil.isAnimated(contentType) {
return "🎡"
return "🎡" // stringlint:disable
}
return "📎"
return "📎" // stringlint:disable
}
public var description: String {

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import GRDB

View File

@ -1,4 +1,6 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import CryptoKit

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import Combine

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import SessionUtilitiesKit

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import SessionUtilitiesKit

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation

View File

@ -1,3 +1,5 @@
// stringlint:disable
import Foundation
public struct HTMLMetadata: Equatable {

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import Combine

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import GRDB

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import GRDB

View File

@ -1,4 +1,6 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import Sodium

View File

@ -1,3 +1,5 @@
// stringlint:disable
import SessionSnodeKit
import SessionUtilitiesKit

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation

View File

@ -6,7 +6,7 @@ import SessionUtilitiesKit
enum _001_InitialSetupMigration: Migration {
static let target: TargetMigrations.Identifier = .snodeKit
static let identifier: String = "initialSetup"
static let identifier: String = "initialSetup" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1

View File

@ -8,7 +8,7 @@ import SessionUtilitiesKit
/// before running the `YDBToGRDBMigration`
enum _002_SetupStandardJobs: Migration {
static let target: TargetMigrations.Identifier = .snodeKit
static let identifier: String = "SetupStandardJobs"
static let identifier: String = "SetupStandardJobs" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import GRDB

View File

@ -7,7 +7,7 @@ import SessionUtilitiesKit
enum _004_FlagMessageHashAsDeletedOrInvalid: Migration {
static let target: TargetMigrations.Identifier = .snodeKit
static let identifier: String = "FlagMessageHashAsDeletedOrInvalid"
static let identifier: String = "FlagMessageHashAsDeletedOrInvalid" // stringlint:disable
static let needsConfigSync: Bool = false
/// This migration adds a flat to the `SnodeReceivedMessageInfo` so that when deleting interactions we can

View File

@ -1,3 +1,5 @@
// stringlint:disable
import Foundation
public extension Notification.Name {

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import Combine

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import Combine

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import SessionUtilitiesKit

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation

View File

@ -8,7 +8,7 @@ import SessionUtilitiesKit
/// theme preferences
enum _001_ThemePreferences: Migration {
static let target: TargetMigrations.Identifier = .uiKit
static let identifier: String = "ThemePreferences"
static let identifier: String = "ThemePreferences" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1
@ -18,11 +18,11 @@ enum _001_ThemePreferences: Migration {
let isExistingUser: Bool = Identity.userExists(db)
let hadCustomLegacyThemeSetting: Bool = UserDefaults.standard.dictionaryRepresentation()
.keys
.contains("appMode")
.contains("appMode") // stringlint:disable
let matchSystemNightModeSetting: Bool = (isExistingUser && !hadCustomLegacyThemeSetting)
let targetTheme: Theme = (!hadCustomLegacyThemeSetting ?
Theme.classicDark :
(UserDefaults.standard.integer(forKey: "appMode") == 0 ?
(UserDefaults.standard.integer(forKey: "appMode") == 0 ? // stringlint:disable
Theme.classicLight :
Theme.classicDark
)

View File

@ -4,7 +4,7 @@ import Foundation
public enum Hex {
public static func isValid(_ string: String) -> Bool {
let allowedCharacters = CharacterSet(charactersIn: "0123456789ABCDEF")
let allowedCharacters = CharacterSet(charactersIn: "0123456789ABCDEF") // stringlint:disable
return string.uppercased().unicodeScalars.allSatisfy { allowedCharacters.contains($0) }
}
@ -32,7 +32,7 @@ public extension Array where Element == UInt8 {
self.reserveCapacity(hex.unicodeScalars.lazy.underestimatedCount)
var buffer: UInt8?
var skip = (hex.hasPrefix("0x") ? 2 : 0)
var skip = (hex.hasPrefix("0x") ? 2 : 0) // stringlint:disable
for char in hex.unicodeScalars.lazy {
guard skip == 0 else {
@ -73,7 +73,7 @@ public extension Array where Element == UInt8 {
}
func toHexString() -> String {
return map { String(format: "%02x", $0) }.joined()
return map { String(format: "%02x", $0) }.joined() // stringlint:disable
}
func toBase64(options: Data.Base64EncodingOptions = []) -> String {

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import YapDatabase

View File

@ -5,7 +5,7 @@ import GRDB
enum _001_InitialSetupMigration: Migration {
static let target: TargetMigrations.Identifier = .utilitiesKit
static let identifier: String = "initialSetup"
static let identifier: String = "initialSetup" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1

View File

@ -7,7 +7,7 @@ import GRDB
/// before running the `YDBToGRDBMigration`
enum _002_SetupStandardJobs: Migration {
static let target: TargetMigrations.Identifier = .utilitiesKit
static let identifier: String = "SetupStandardJobs"
static let identifier: String = "SetupStandardJobs" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1

View File

@ -6,7 +6,7 @@ import YapDatabase
enum _003_YDBToGRDBMigration: Migration {
static let target: TargetMigrations.Identifier = .utilitiesKit
static let identifier: String = "YDBToGRDBMigration"
static let identifier: String = "YDBToGRDBMigration" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1
@ -28,7 +28,7 @@ enum _003_YDBToGRDBMigration: Migration {
// Map the Legacy types for the NSKeyedUnarchiver
NSKeyedUnarchiver.setClass(
SUKLegacy.KeyPair.self,
forClassName: "ECKeyPair"
forClassName: "ECKeyPair" // stringlint:disable
)
dbConnection.read { transaction in

View File

@ -6,7 +6,7 @@ import YapDatabase
enum _004_AddJobPriority: Migration {
static let target: TargetMigrations.Identifier = .utilitiesKit
static let identifier: String = "AddJobPriority"
static let identifier: String = "AddJobPriority" // stringlint:disable
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1

View File

@ -1,3 +1,5 @@
// stringlint:disable
#import "OWSFileSystem.h"
#import "AppContext.h"

View File

@ -1,6 +1,7 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
// stringlint:disable
import Foundation
import SAMKeychain

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import CryptoKit

View File

@ -1,4 +1,6 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import GRDB

View File

@ -1,3 +1,5 @@
// stringlint:disable
#import "AppContext.h"
NS_ASSUME_NONNULL_BEGIN

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import SignalCoreKit

View File

@ -62,7 +62,7 @@ public enum SNUserDefaults {
}
public extension UserDefaults {
public static let applicationGroup: String = "group.com.loki-project.loki-messenger"
static let applicationGroup: String = "group.com.loki-project.loki-messenger"
@objc static var sharedLokiProject: UserDefaults? {
UserDefaults(suiteName: UserDefaults.applicationGroup)

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import GRDB

View File

@ -1,3 +1,5 @@
// stringlint:disable
#import "MIMETypeUtil.h"
#import "OWSFileSystem.h"

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import Combine

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation

View File

@ -1,4 +1,6 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation

View File

@ -1,4 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation

View File

@ -1,6 +1,7 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
// stringlint:disable
#import "AppVersion.h"
#import "NSUserDefaults+OWS.h"