Merge branch 'updated-user-config-handling' into disappearing-message-redesign

pull/941/head
Ryan Zhao 12 months ago
commit 0b0371e769

@ -101,6 +101,7 @@ end
# Actions to perform post-install
post_install do |installer|
set_minimum_deployment_target(installer)
avoid_rsync_webrtc_if_unchanged(installer)
end
def set_minimum_deployment_target(installer)
@ -110,3 +111,12 @@ def set_minimum_deployment_target(installer)
end
end
end
# This function patches the Cocoapods 'Embed Frameworks' script to avoid running rsync
# for the WebRTC-lib framework in simulator builds if it has already been copied over
# because due to the size it can take over 10 seconds to embed, and gets embeded in
# each target on every build regardless of whether there were changes, drastically
# increasing the length of the build
def avoid_rsync_webrtc_if_unchanged(installer)
system('find "./Pods/Target Support Files" -name "*-frameworks.sh" -exec patch -p0 -i ./Scripts/skip_web_rtc_re_rsync.patch {} \;')
end

@ -204,6 +204,6 @@ SPEC CHECKSUMS:
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 68799237a4dc046f5ac25c573af03b559f5b10c4
PODFILE CHECKSUM: dcca0c4ad69b14cbc2d6ba49f9d690b239828e6d
COCOAPODS: 1.12.1

@ -0,0 +1,12 @@
@@ -41,0 +41,11 @@
+ # Skip the rsync step for the WebRTC-lib in simulator builds
+ if [[ "$PLATFORM_NAME" == "iphonesimulator" ]] && [[ "$source" == *WebRTC.framework* ]]; then
+ if [[ -f "${source}/../already_rsynced.nonce" ]]; then
+ echo "Already rsynced WebRTC, skipping"
+ return 0
+ fi
+
+ echo "About to rsync a simulator WebRTC, creating nonce to prevent future rsyncing"
+ touch "${source}/../already_rsynced.nonce"
+ fi
+

@ -703,6 +703,7 @@
FD716E6C28505E1C00C96BF4 /* MessageRequestsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD716E6B28505E1C00C96BF4 /* MessageRequestsViewModel.swift */; };
FD716E7128505E5200C96BF4 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD716E7028505E5100C96BF4 /* MessageRequestsCell.swift */; };
FD716E722850647600C96BF4 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF127BF6BA200510D0C /* Data+Utilities.swift */; };
FD7692F72A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7692F62A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift */; };
FD7728962849E7E90018502F /* String+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728952849E7E90018502F /* String+Utilities.swift */; };
FD7728982849E8110018502F /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728972849E8110018502F /* UITableView+ReusableView.swift */; };
FD77289A284AF1BD0018502F /* Sodium+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD772899284AF1BD0018502F /* Sodium+Utilities.swift */; };
@ -1846,6 +1847,7 @@
FD716E692850327900C96BF4 /* EndCallMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndCallMode.swift; sourceTree = "<group>"; };
FD716E6B28505E1C00C96BF4 /* MessageRequestsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewModel.swift; sourceTree = "<group>"; };
FD716E7028505E5100C96BF4 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = "<group>"; };
FD7692F62A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModelSpec.swift; sourceTree = "<group>"; };
FD7728952849E7E90018502F /* String+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Utilities.swift"; sourceTree = "<group>"; };
FD7728972849E8110018502F /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = "<group>"; };
FD772899284AF1BD0018502F /* Sodium+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Sodium+Utilities.swift"; sourceTree = "<group>"; };
@ -3346,10 +3348,10 @@
B8DE1FB226C22F1F0079C9CE /* Calls */,
C32C5BCB256DC818003C73A2 /* Database */,
C300A5BB2554AFFB00555489 /* Messages */,
C300A5F02554B08500555489 /* Sending & Receiving */,
C352A2F325574B3300338F3E /* Jobs */,
C3A7215C2558C0AC0043A11F /* File Server */,
C3A721332558BDDF0043A11F /* Open Groups */,
C300A5F02554B08500555489 /* Sending & Receiving */,
FD8ECF7529340F4800C0D1BB /* SessionUtil */,
FD3E0C82283B581F002A425C /* Shared Models */,
C3BBE0B32554F0D30050F1E3 /* Utilities */,
@ -4027,6 +4029,14 @@
path = Views;
sourceTree = "<group>";
};
FD7692F52A53A2C7000E4B70 /* Shared Models */ = {
isa = PBXGroup;
children = (
FD7692F62A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift */,
);
path = "Shared Models";
sourceTree = "<group>";
};
FD7728A1284F0DF50018502F /* Message Handling */ = {
isa = PBXGroup;
children = (
@ -4258,6 +4268,7 @@
FD3C906527E416A200CD579F /* Contacts */,
FDC4389827BA001800C60D73 /* Open Groups */,
FD3C906B27E43C2400CD579F /* Sending & Receiving */,
FD7692F52A53A2C7000E4B70 /* Shared Models */,
FD3C906827E417B100CD579F /* Utilities */,
FD8ECF802934385900C0D1BB /* LibSessionUtil */,
);
@ -6441,6 +6452,7 @@
FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */,
FD8ECF822934387A00C0D1BB /* ConfigUserProfileSpec.swift in Sources */,
FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */,
FD7692F72A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift in Sources */,
FDC2908F27D70938005DAE71 /* SendDirectMessageRequestSpec.swift in Sources */,
FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */,
FDA1E83929A5771A00C5C3BD /* LibSessionSpec.swift in Sources */,
@ -6637,7 +6649,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 414;
CURRENT_PROJECT_VERSION = 415;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6709,7 +6721,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 414;
CURRENT_PROJECT_VERSION = 415;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -6774,7 +6786,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 414;
CURRENT_PROJECT_VERSION = 415;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6848,7 +6860,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 414;
CURRENT_PROJECT_VERSION = 415;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -7756,7 +7768,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 414;
CURRENT_PROJECT_VERSION = 415;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -7827,7 +7839,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 414;
CURRENT_PROJECT_VERSION = 415;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",

@ -2010,8 +2010,7 @@ extension ConversationVC:
self?.showInputAccessoryView()
})
self.inputAccessoryView?.isHidden = true
self.inputAccessoryView?.alpha = 0
self.hideInputAccessoryView()
Modal.setupForIPadIfNeeded(actionSheet, targetView: self.view)
self.present(actionSheet, animated: true)
}

@ -106,6 +106,7 @@ class MessageRequestsViewController: BaseVC, SessionUtilRespondingViewController
result.translatesAutoresizingMaskIntoConstraints = false
result.setTitle("MESSAGE_REQUESTS_CLEAR_ALL".localized(), for: .normal)
result.addTarget(self, action: #selector(clearAllTapped), for: .touchUpInside)
result.accessibilityIdentifier = "Clear all"
return result
}()

@ -42,75 +42,97 @@ public enum SyncPushTokensJob: JobExecutor {
}
}()
// Push tokens don't normally change while the app is launched, so you would assume checking once
// during launch is sufficient, but e.g. on iOS11, users who have disabled "Allow Notifications"
// and disabled "Background App Refresh" will not be able to obtain an APN token. Enabling those
// settings does not restart the app, so we check every activation for users who haven't yet
// registered.
//
// It's also possible for a device to successfully register for push notifications but fail to
// register with Session
//
// Due to the above we want to re-register at least once every ~12 hours to ensure users will
// continue to receive push notifications
//
// In addition to this if we are custom running the job (eg. by toggling the push notification
// setting) then we should run regardless of the other settings so users have a mechanism to force
// the registration to run
let lastPushNotificationSync: Date = UserDefaults.standard[.lastPushNotificationSync]
.defaulting(to: Date.distantPast)
guard
job.behaviour == .runOnce ||
!isRegisteredForRemoteNotifications ||
Date().timeIntervalSince(lastPushNotificationSync) >= SyncPushTokensJob.maxFrequency
else {
SNLog("[SyncPushTokensJob] Deferred due to Fast Mode disabled or recent-enough registration")
// Apple's documentation states that we should re-register for notifications on every launch:
// https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/HandlingRemoteNotifications.html#//apple_ref/doc/uid/TP40008194-CH6-SW1
guard job.behaviour == .runOnce || !isRegisteredForRemoteNotifications else {
SNLog("[SyncPushTokensJob] Deferred due to Fast Mode disabled")
deferred(job) // Don't need to do anything if push notifications are already registered
return
}
Logger.info("Re-registering for remote notifications.")
// Determine if the device has 'Fast Mode' (APNS) enabled
let isUsingFullAPNs: Bool = UserDefaults.standard[.isUsingFullAPNs]
// Perform device registration
PushRegistrationManager.shared.requestPushTokens()
.subscribe(on: queue)
.flatMap { (pushToken: String, voipToken: String) -> AnyPublisher<Void, Error> in
Deferred {
Future<Void, Error> { resolver in
SyncPushTokensJob.registerForPushNotifications(
pushToken: pushToken,
voipToken: voipToken,
isForcedUpdate: true,
success: { resolver(Result.success(())) },
failure: { resolver(Result.failure($0)) }
)
// If the job is running and 'Fast Mode' is disabled then we should try to unregister the existing
// token
guard isUsingFullAPNs else {
Just(Storage.shared[.lastRecordedPushToken])
.setFailureType(to: Error.self)
.flatMap { lastRecordedPushToken in
if let existingToken: String = lastRecordedPushToken {
SNLog("[SyncPushTokensJob] Unregister using last recorded push token: \(redact(existingToken))")
return Just(existingToken)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
SNLog("[SyncPushTokensJob] Unregister using live token provided from device")
return PushRegistrationManager.shared.requestPushTokens()
.map { token, _ in token }
.eraseToAnyPublisher()
}
.handleEvents(
.flatMap { pushToken in PushNotificationAPI.unregister(Data(hex: pushToken)) }
.map {
// Tell the device to unregister for remote notifications (essentially try to invalidate
// the token if needed
DispatchQueue.main.sync { UIApplication.shared.unregisterForRemoteNotifications() }
Storage.shared.write { db in
db[.lastRecordedPushToken] = nil
}
return ()
}
.subscribe(on: queue)
.sinkUntilComplete(
receiveCompletion: { result in
switch result {
case .failure(let error):
SNLog("[SyncPushTokensJob] Failed to register due to error: \(error)")
case .finished:
Logger.warn("Recording push tokens locally. pushToken: \(redact(pushToken)), voipToken: \(redact(voipToken))")
SNLog("[SyncPushTokensJob] Completed")
UserDefaults.standard[.lastPushNotificationSync] = Date()
Storage.shared.write { db in
db[.lastRecordedPushToken] = pushToken
db[.lastRecordedVoipToken] = voipToken
}
case .finished: SNLog("[SyncPushTokensJob] Unregister Completed")
case .failure: SNLog("[SyncPushTokensJob] Unregister Failed")
}
// We want to complete this job regardless of success or failure
success(job, false)
}
)
.eraseToAnyPublisher()
return
}
// Perform device registration
Logger.info("Re-registering for remote notifications.")
PushRegistrationManager.shared.requestPushTokens()
.flatMap { (pushToken: String, voipToken: String) -> AnyPublisher<Void, Error> in
PushNotificationAPI
.register(
with: Data(hex: pushToken),
publicKey: getUserHexEncodedPublicKey(),
isForcedUpdate: true
)
.retry(3)
.handleEvents(
receiveCompletion: { result in
switch result {
case .failure(let error):
SNLog("[SyncPushTokensJob] Failed to register due to error: \(error)")
case .finished:
Logger.warn("Recording push tokens locally. pushToken: \(redact(pushToken)), voipToken: \(redact(voipToken))")
SNLog("[SyncPushTokensJob] Completed")
UserDefaults.standard[.lastPushNotificationSync] = Date()
Storage.shared.write { db in
db[.lastRecordedPushToken] = pushToken
db[.lastRecordedVoipToken] = voipToken
}
}
}
)
.map { _ in () }
.eraseToAnyPublisher()
}
.subscribe(on: queue)
.sinkUntilComplete(
// We want to complete this job regardless of success or failure
receiveCompletion: { _ in success(job, false) },
receiveValue: { _ in }
receiveCompletion: { _ in success(job, false) }
)
}
@ -147,68 +169,3 @@ extension SyncPushTokensJob {
private func redact(_ string: String) -> String {
return OWSIsDebugBuild() ? string : "[ READACTED \(string.prefix(2))...\(string.suffix(2)) ]"
}
extension SyncPushTokensJob {
fileprivate static func registerForPushNotifications(
pushToken: String,
voipToken: String,
isForcedUpdate: Bool,
success: @escaping () -> (),
failure: @escaping (Error) -> (),
remainingRetries: Int = 3
) {
let isUsingFullAPNs: Bool = UserDefaults.standard[.isUsingFullAPNs]
Just(Data(hex: pushToken))
.setFailureType(to: Error.self)
.flatMap { pushTokenAsData -> AnyPublisher<Bool, Error> in
guard isUsingFullAPNs else {
return PushNotificationAPI.unregister(pushTokenAsData)
.map { _ in true }
.eraseToAnyPublisher()
}
return PushNotificationAPI
.register(
with: pushTokenAsData,
publicKey: getUserHexEncodedPublicKey(),
isForcedUpdate: isForcedUpdate
)
.map { _ in true }
.eraseToAnyPublisher()
}
.catch { error -> AnyPublisher<Bool, Error> in
guard remainingRetries == 0 else {
SyncPushTokensJob.registerForPushNotifications(
pushToken: pushToken,
voipToken: voipToken,
isForcedUpdate: isForcedUpdate,
success: success,
failure: failure,
remainingRetries: (remainingRetries - 1)
)
return Just(false)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
return Fail(error: error)
.eraseToAnyPublisher()
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete(
receiveCompletion: { result in
switch result {
case .finished: break
case .failure(let error): failure(error)
}
},
receiveValue: { didComplete in
guard didComplete else { return }
success()
}
)
}
}

@ -1131,19 +1131,22 @@ public extension SessionThreadViewModel {
/// Step 2 - Separate any words outside of quotes
/// Step 3 - Join the different search term parts with 'OR" (include results for each individual term)
/// Step 4 - Append a wild-card character to the final word (as long as the last word doesn't end in a quote)
return standardQuotes(searchTerm)
.split(separator: "\"")
.enumerated()
.flatMap { index, value -> [String] in
guard index % 2 == 1 else {
return String(value)
.split(separator: " ")
.map { "\"\(String($0))\"" }
}
return ["\"\(value)\""]
}
.filter { !$0.isEmpty }
let normalisedTerm: String = standardQuotes(searchTerm)
guard let regex = try? NSRegularExpression(pattern: "[^\\s\"']+|\"([^\"]*)\"") else {
// Fallback to removing the quotes and just splitting on spaces
return normalisedTerm
.replacingOccurrences(of: "\"", with: "")
.split(separator: " ")
.map { "\"\($0)\"" }
.filter { !$0.isEmpty }
}
return regex
.matches(in: normalisedTerm, range: NSRange(location: 0, length: normalisedTerm.count))
.compactMap { Range($0.range, in: normalisedTerm) }
.map { normalisedTerm[$0].trimmingCharacters(in: CharacterSet(charactersIn: "\"")) }
.map { "\"\($0)\"" }
}
static func standardQuotes(_ term: String) -> String {
@ -1174,15 +1177,17 @@ public extension SessionThreadViewModel {
/// There are cases where creating a pattern can fail, we want to try and recover from those cases
/// by failling back to simpler patterns if needed
let maybePattern: FTS5Pattern? = (try? db.makeFTS5Pattern(rawPattern: rawPattern, forTable: table))
.defaulting(
to: (try? db.makeFTS5Pattern(rawPattern: fallbackTerm, forTable: table))
.defaulting(to: FTS5Pattern(matchingAnyTokenIn: fallbackTerm))
)
guard let pattern: FTS5Pattern = maybePattern else { throw StorageError.invalidSearchPattern }
return pattern
return try {
if let pattern: FTS5Pattern = try? db.makeFTS5Pattern(rawPattern: rawPattern, forTable: table) {
return pattern
}
if let pattern: FTS5Pattern = try? db.makeFTS5Pattern(rawPattern: fallbackTerm, forTable: table) {
return pattern
}
return try FTS5Pattern(matchingAnyTokenIn: fallbackTerm) ?? { throw StorageError.invalidSearchPattern }()
}()
}
static func messagesQuery(userPublicKey: String, pattern: FTS5Pattern) -> AdaptedFetchRequest<SQLRequest<SessionThreadViewModel>> {

@ -0,0 +1,334 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import Quick
import Nimble
import SessionUtilitiesKit
@testable import SessionMessagingKit
class SessionThreadViewModelSpec: QuickSpec {
public struct TestMessage: Codable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "testMessage" }
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case body
}
public let body: String
}
// MARK: - Spec
override func spec() {
describe("a SessionThreadViewModel") {
var mockStorage: Storage!
beforeEach {
mockStorage = SynchronousStorage(
customWriter: try! DatabaseQueue()
)
mockStorage.write { db in
try db.create(table: TestMessage.self) { t in
t.column(.body, .text).notNull()
}
try db.create(virtualTable: TestMessage.fullTextSearchTableName, using: FTS5()) { t in
t.synchronize(withTable: TestMessage.databaseTableName)
t.tokenizer = .porter(wrapping: .unicode61())
t.column(TestMessage.Columns.body.name)
}
}
}
// MARK: - when processing a search term
context("when processing a search term") {
// MARK: -- correctly generates a safe search term
it("correctly generates a safe search term") {
expect(SessionThreadViewModel.searchSafeTerm("Test")).to(equal("\"Test\""))
}
// MARK: -- standardises odd quote characters
it("standardises odd quote characters") {
expect(SessionThreadViewModel.standardQuotes("\"")).to(equal("\""))
expect(SessionThreadViewModel.standardQuotes("")).to(equal("\""))
expect(SessionThreadViewModel.standardQuotes("")).to(equal("\""))
}
// MARK: -- splits on the space character
it("splits on the space character") {
expect(SessionThreadViewModel.searchTermParts("Test Message"))
.to(equal([
"\"Test\"",
"\"Message\""
]))
}
// MARK: -- surrounds each split term with quotes
it("surrounds each split term with quotes") {
expect(SessionThreadViewModel.searchTermParts("Test Message"))
.to(equal([
"\"Test\"",
"\"Message\""
]))
}
// MARK: -- keeps words within quotes together
it("keeps words within quotes together") {
expect(SessionThreadViewModel.searchTermParts("This \"is a Test\" Message"))
.to(equal([
"\"This\"",
"\"is a Test\"",
"\"Message\""
]))
expect(SessionThreadViewModel.searchTermParts("\"This is\" a Test Message"))
.to(equal([
"\"This is\"",
"\"a\"",
"\"Test\"",
"\"Message\""
]))
expect(SessionThreadViewModel.searchTermParts("\"This is\" \"a Test\" Message"))
.to(equal([
"\"This is\"",
"\"a Test\"",
"\"Message\""
]))
expect(SessionThreadViewModel.searchTermParts("\"This is\" a \"Test Message\""))
.to(equal([
"\"This is\"",
"\"a\"",
"\"Test Message\""
]))
expect(SessionThreadViewModel.searchTermParts("\"This is\"\" a \"Test Message"))
.to(equal([
"\"This is\"",
"\" a \"",
"\"Test\"",
"\"Message\""
]))
}
// MARK: -- keeps words within weird quotes together
it("keeps words within weird quotes together") {
expect(SessionThreadViewModel.searchTermParts("This ”is a Test“ Message"))
.to(equal([
"\"This\"",
"\"is a Test\"",
"\"Message\""
]))
}
// MARK: -- removes extra whitespace
it("removes extra whitespace") {
expect(SessionThreadViewModel.searchTermParts(" Test Message "))
.to(equal([
"\"Test\"",
"\"Message\""
]))
}
}
// MARK: - when searching
context("when searching") {
beforeEach {
mockStorage.write { db in
try TestMessage(body: "Test").insert(db)
try TestMessage(body: "Test123").insert(db)
try TestMessage(body: "Test234").insert(db)
try TestMessage(body: "Test Test123").insert(db)
try TestMessage(body: "Test Test123 Test234").insert(db)
try TestMessage(body: "Test Test234").insert(db)
try TestMessage(body: "Test Test234 Test123").insert(db)
try TestMessage(body: "This is a Test Message").insert(db)
try TestMessage(body: "is a Message This Test").insert(db)
try TestMessage(body: "this message is a test").insert(db)
try TestMessage(
body: "This content is something which includes a combination of test words found in another message"
)
.insert(db)
try TestMessage(body: "Do test messages contain content?").insert(db)
try TestMessage(body: "Is messaging awesome?").insert(db)
}
}
// MARK: -- returns results
it("returns results") {
let results = mockStorage.read { db in
let pattern: FTS5Pattern = try SessionThreadViewModel.pattern(
db,
searchTerm: "Message",
forTable: TestMessage.self
)
return try SQLRequest<TestMessage>(literal: """
SELECT *
FROM testMessage
JOIN testMessage_fts ON (
testMessage_fts.rowId = testMessage.rowId AND
testMessage_fts.body MATCH \(pattern)
)
""").fetchAll(db)
}
expect(results)
.to(equal([
TestMessage(body: "This is a Test Message"),
TestMessage(body: "is a Message This Test"),
TestMessage(body: "this message is a test"),
TestMessage(body: "This content is something which includes a combination of test words found in another message"),
TestMessage(body: "Do test messages contain content?"),
TestMessage(body: "Is messaging awesome?")
]))
}
// MARK: -- adds a wildcard to the final part
it("adds a wildcard to the final part") {
let results = mockStorage.read { db in
let pattern: FTS5Pattern = try SessionThreadViewModel.pattern(
db,
searchTerm: "This mes",
forTable: TestMessage.self
)
return try SQLRequest<TestMessage>(literal: """
SELECT *
FROM testMessage
JOIN testMessage_fts ON (
testMessage_fts.rowId = testMessage.rowId AND
testMessage_fts.body MATCH \(pattern)
)
""").fetchAll(db)
}
expect(results)
.to(equal([
TestMessage(body: "This is a Test Message"),
TestMessage(body: "is a Message This Test"),
TestMessage(body: "this message is a test"),
TestMessage(body: "This content is something which includes a combination of test words found in another message"),
TestMessage(body: "Do test messages contain content?"),
TestMessage(body: "Is messaging awesome?")
]))
}
// MARK: -- does not add a wildcard to other parts
it("does not add a wildcard to other parts") {
let results = mockStorage.read { db in
let pattern: FTS5Pattern = try SessionThreadViewModel.pattern(
db,
searchTerm: "mes Random",
forTable: TestMessage.self
)
return try SQLRequest<TestMessage>(literal: """
SELECT *
FROM testMessage
JOIN testMessage_fts ON (
testMessage_fts.rowId = testMessage.rowId AND
testMessage_fts.body MATCH \(pattern)
)
""").fetchAll(db)
}
expect(results)
.to(beEmpty())
}
// MARK: -- finds similar words without the wildcard due to the porter tokenizer
it("finds similar words without the wildcard due to the porter tokenizer") {
let results = mockStorage.read { db in
let pattern: FTS5Pattern = try SessionThreadViewModel.pattern(
db,
searchTerm: "message z",
forTable: TestMessage.self
)
return try SQLRequest<TestMessage>(literal: """
SELECT *
FROM testMessage
JOIN testMessage_fts ON (
testMessage_fts.rowId = testMessage.rowId AND
testMessage_fts.body MATCH \(pattern)
)
""").fetchAll(db)
}
expect(results)
.to(equal([
TestMessage(body: "This is a Test Message"),
TestMessage(body: "is a Message This Test"),
TestMessage(body: "this message is a test"),
TestMessage(
body: "This content is something which includes a combination of test words found in another message"
),
TestMessage(body: "Do test messages contain content?"),
TestMessage(body: "Is messaging awesome?")
]))
}
// MARK: -- finds results containing the words regardless of the order
it("finds results containing the words regardless of the order") {
let results = mockStorage.read { db in
let pattern: FTS5Pattern = try SessionThreadViewModel.pattern(
db,
searchTerm: "is a message",
forTable: TestMessage.self
)
return try SQLRequest<TestMessage>(literal: """
SELECT *
FROM testMessage
JOIN testMessage_fts ON (
testMessage_fts.rowId = testMessage.rowId AND
testMessage_fts.body MATCH \(pattern)
)
""").fetchAll(db)
}
expect(results)
.to(equal([
TestMessage(body: "This is a Test Message"),
TestMessage(body: "is a Message This Test"),
TestMessage(body: "this message is a test"),
TestMessage(
body: "This content is something which includes a combination of test words found in another message"
),
TestMessage(body: "Do test messages contain content?"),
TestMessage(body: "Is messaging awesome?")
]))
}
// MARK: -- does not find quoted parts out of order
it("does not find quoted parts out of order") {
let results = mockStorage.read { db in
let pattern: FTS5Pattern = try SessionThreadViewModel.pattern(
db,
searchTerm: "\"this is a\" \"test message\"",
forTable: TestMessage.self
)
return try SQLRequest<TestMessage>(literal: """
SELECT *
FROM testMessage
JOIN testMessage_fts ON (
testMessage_fts.rowId = testMessage.rowId AND
testMessage_fts.body MATCH \(pattern)
)
""").fetchAll(db)
}
expect(results)
.to(equal([
TestMessage(body: "This is a Test Message"),
TestMessage(body: "Do test messages contain content?")
]))
}
}
}
}
}

@ -237,7 +237,8 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
},
migrationsCompletion: { [weak self] result, needsConfigSync in
switch result {
case .failure: SNLog("[NotificationServiceExtension] Failed to complete migrations")
// Only 'NSLog' works in the extension - viewable via Console.app
case .failure: NSLog("[NotificationServiceExtension] Failed to complete migrations")
case .success:
DispatchQueue.main.async {
self?.versionMigrationsDidComplete(needsConfigSync: needsConfigSync)

@ -4,6 +4,8 @@ import Combine
import GRDB
import Quick
import Nimble
import SessionUIKit
import SessionSnodeKit
@testable import Session

@ -4,6 +4,8 @@ import Combine
import GRDB
import Quick
import Nimble
import SessionUIKit
import SessionSnodeKit
@testable import Session

Loading…
Cancel
Save