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:
parent
bfc5375a30
commit
5917cf103f
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)",
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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).")
|
||||
|
|
|
@ -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
|
||||
}()
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 []
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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: ", ")))")
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import Sodium
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import AVKit
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import SessionUtilitiesKit
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import SessionUtilitiesKit
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct HTMLMetadata: Equatable {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import Sodium
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// stringlint:disable
|
||||
|
||||
import SessionSnodeKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Notification.Name {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import SessionUtilitiesKit
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import YapDatabase
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// stringlint:disable
|
||||
|
||||
#import "OWSFileSystem.h"
|
||||
#import "AppContext.h"
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import SAMKeychain
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// stringlint:disable
|
||||
|
||||
#import "AppContext.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import SignalCoreKit
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// stringlint:disable
|
||||
|
||||
#import "MIMETypeUtil.h"
|
||||
#import "OWSFileSystem.h"
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
import Foundation
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
// stringlint:disable
|
||||
|
||||
#import "AppVersion.h"
|
||||
#import "NSUserDefaults+OWS.h"
|
||||
|
|
Loading…
Reference in New Issue