diff --git a/Podfile b/Podfile index 5747d4620..25f76e8e9 100644 --- a/Podfile +++ b/Podfile @@ -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 diff --git a/Podfile.lock b/Podfile.lock index 970a62e71..d44b761b9 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -204,6 +204,6 @@ SPEC CHECKSUMS: YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 68799237a4dc046f5ac25c573af03b559f5b10c4 +PODFILE CHECKSUM: dcca0c4ad69b14cbc2d6ba49f9d690b239828e6d COCOAPODS: 1.12.1 diff --git a/Scripts/skip_web_rtc_re_rsync.patch b/Scripts/skip_web_rtc_re_rsync.patch new file mode 100644 index 000000000..6b8232e50 --- /dev/null +++ b/Scripts/skip_web_rtc_re_rsync.patch @@ -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 ++ diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 169bf35ab..7e18bd930 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -696,6 +696,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 */; }; @@ -1828,6 +1829,7 @@ FD716E692850327900C96BF4 /* EndCallMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndCallMode.swift; sourceTree = ""; }; FD716E6B28505E1C00C96BF4 /* MessageRequestsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewModel.swift; sourceTree = ""; }; FD716E7028505E5100C96BF4 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = ""; }; + FD7692F62A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModelSpec.swift; sourceTree = ""; }; FD7728952849E7E90018502F /* String+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Utilities.swift"; sourceTree = ""; }; FD7728972849E8110018502F /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = ""; }; FD772899284AF1BD0018502F /* Sodium+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Sodium+Utilities.swift"; sourceTree = ""; }; @@ -3316,10 +3318,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 */, @@ -3994,6 +3996,14 @@ path = Views; sourceTree = ""; }; + FD7692F52A53A2C7000E4B70 /* Shared Models */ = { + isa = PBXGroup; + children = ( + FD7692F62A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift */, + ); + path = "Shared Models"; + sourceTree = ""; + }; FD7728A1284F0DF50018502F /* Message Handling */ = { isa = PBXGroup; children = ( @@ -4224,6 +4234,7 @@ FD3C906527E416A200CD579F /* Contacts */, FDC4389827BA001800C60D73 /* Open Groups */, FD3C906B27E43C2400CD579F /* Sending & Receiving */, + FD7692F52A53A2C7000E4B70 /* Shared Models */, FD3C906827E417B100CD579F /* Utilities */, FD8ECF802934385900C0D1BB /* LibSessionUtil */, ); @@ -6391,6 +6402,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 */, diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 13704aadb..db534d40b 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1979,8 +1979,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) } diff --git a/Session/Home/Message Requests/MessageRequestsViewController.swift b/Session/Home/Message Requests/MessageRequestsViewController.swift index 42cb93344..6b91630da 100644 --- a/Session/Home/Message Requests/MessageRequestsViewController.swift +++ b/Session/Home/Message Requests/MessageRequestsViewController.swift @@ -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 }() diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index 5fd4f3857..bee72d0d0 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -1112,19 +1112,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 { @@ -1155,15 +1158,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> { diff --git a/SessionMessagingKitTests/Shared Models/SessionThreadViewModelSpec.swift b/SessionMessagingKitTests/Shared Models/SessionThreadViewModelSpec.swift new file mode 100644 index 000000000..df58e26c3 --- /dev/null +++ b/SessionMessagingKitTests/Shared Models/SessionThreadViewModelSpec.swift @@ -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(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(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(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(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(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(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?") + ])) + } + } + } + } +} diff --git a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift index f583b8b3c..8c51c3c49 100644 --- a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift @@ -4,6 +4,8 @@ import Combine import GRDB import Quick import Nimble +import SessionUIKit +import SessionSnodeKit @testable import Session diff --git a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift index 2750a3803..eaf4b915b 100644 --- a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift @@ -4,6 +4,8 @@ import Combine import GRDB import Quick import Nimble +import SessionUIKit +import SessionSnodeKit @testable import Session