Fixed additional QA issues

• Updated the "Delete for Everyone" behaviour to show a blocking loading indicator if a network request is required
• Removed some duplicate code
• Fixed an issue where mentions wouldn't filter correctly for groups in some cases
• Fixed an issue where legacy group members could appear in the members list multiple times when navigating there from the conversation title
• Fixed an issue where the message deletion toasts could appear behind blurred modal backgrounds
• Fixed an issue where pasting text which is too large into an empty input field would result in the text having the default OS styling instead of the text styling we have set
pull/894/head
Morgan Pretty 2 months ago
parent 9ae785ae3a
commit dce05376d1

@ -182,7 +182,6 @@
947AD6902C8968FF000B2730 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947AD68F2C8968FF000B2730 /* Constants.swift */; }; 947AD6902C8968FF000B2730 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947AD68F2C8968FF000B2730 /* Constants.swift */; };
94B3DC172AF8592200C88531 /* QuoteView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */; }; 94B3DC172AF8592200C88531 /* QuoteView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */; };
94C58AC92D2E037200609195 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C58AC82D2E036E00609195 /* Permissions.swift */; }; 94C58AC92D2E037200609195 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C58AC82D2E036E00609195 /* Permissions.swift */; };
94C5DCB02BE88170003AA8C5 /* BezierPathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C5DCAF2BE88170003AA8C5 /* BezierPathView.swift */; };
94E9BC0D2C7BFBDA006984EA /* Localization+Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */; }; 94E9BC0D2C7BFBDA006984EA /* Localization+Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */; };
A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; }; A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; };
A163E8AB16F3F6AA0094D68B /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A163E8AA16F3F6A90094D68B /* Security.framework */; }; A163E8AB16F3F6AA0094D68B /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A163E8AA16F3F6A90094D68B /* Security.framework */; };
@ -1454,7 +1453,6 @@
947AD68F2C8968FF000B2730 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; }; 947AD68F2C8968FF000B2730 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView_SwiftUI.swift; sourceTree = "<group>"; }; 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView_SwiftUI.swift; sourceTree = "<group>"; };
94C58AC82D2E036E00609195 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = "<group>"; }; 94C58AC82D2E036E00609195 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = "<group>"; };
94C5DCAF2BE88170003AA8C5 /* BezierPathView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BezierPathView.swift; sourceTree = "<group>"; };
94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Localization+Style.swift"; sourceTree = "<group>"; }; 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Localization+Style.swift"; sourceTree = "<group>"; };
A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
A163E8AA16F3F6A90094D68B /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; A163E8AA16F3F6A90094D68B /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
@ -3721,11 +3719,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
94C58AC82D2E036E00609195 /* Permissions.swift */, 94C58AC82D2E036E00609195 /* Permissions.swift */,
FD6A39422C2AD81600762359 /* BackgroundTaskManager.swift */,
FDFBB74A2A1EFF4900CA7350 /* Bencode.swift */,
FD6A39162C2A99A000762359 /* BencodeDecoder.swift */,
FD6A39182C2A99AB00762359 /* BencodeEncoder.swift */,
FD6A391A2C2A99B600762359 /* BencodeResponse.swift */,
FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */, FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */,
FD7443452D07CA9F00862443 /* CGFloat+Utilities.swift */, FD7443452D07CA9F00862443 /* CGFloat+Utilities.swift */,
FD7443462D07CA9F00862443 /* CGPoint+Utilities.swift */, FD7443462D07CA9F00862443 /* CGPoint+Utilities.swift */,
@ -7598,7 +7591,6 @@
CODE_SIGN_IDENTITY = ""; CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
@ -7927,7 +7919,7 @@
CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CURRENT_PROJECT_VERSION = 530; CURRENT_PROJECT_VERSION = 531;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
@ -8003,7 +7995,7 @@
CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_IDENTITY = "iPhone Distribution";
CURRENT_PROJECT_VERSION = 530; CURRENT_PROJECT_VERSION = 531;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_MODULE_VERIFIER = YES; ENABLE_MODULE_VERIFIER = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
@ -8066,7 +8058,6 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 528;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -8104,7 +8095,6 @@
"$(SRCROOT)", "$(SRCROOT)",
); );
LLVM_LTO = NO; LLVM_LTO = NO;
MARKETING_VERSION = 2.8.7;
OTHER_LDFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
@ -8134,7 +8124,6 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 528;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -8172,7 +8161,6 @@
"$(SRCROOT)", "$(SRCROOT)",
); );
LLVM_LTO = NO; LLVM_LTO = NO;
MARKETING_VERSION = 2.8.7;
OTHER_LDFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session; PRODUCT_NAME = Session;

@ -46,40 +46,19 @@ extension ConversationVC:
navigationController?.pushViewController(viewController, animated: true) navigationController?.pushViewController(viewController, animated: true)
case (.userCount, .group, true, _), (.userCount, .legacyGroup, true, _): case (.userCount, .group, true, _), (.userCount, .legacyGroup, true, _):
let viewController: SessionTableViewController = SessionTableViewController( let viewController: UIViewController = ThreadSettingsViewModel.createMemberListViewController(
viewModel: UserListViewModel( threadId: self.viewModel.threadData.threadId,
title: "groupMembers".localized(), transitionToConversation: { [weak self, dependencies = viewModel.dependencies] selectedMemberId in
showProfileIcons: true, self?.navigationController?.pushViewController(
request: GroupMember ConversationVC(
.filter(GroupMember.Columns.groupId == self.viewModel.threadData.threadId), threadId: selectedMemberId,
onTap: .callback { [weak self, dependencies = viewModel.dependencies] _, memberInfo in threadVariant: .contact,
dependencies[singleton: .storage].write { db in using: dependencies
try SessionThread.upsert( ),
db, animated: true
id: memberInfo.profileId, )
variant: .contact, },
values: SessionThread.TargetValues( using: viewModel.dependencies
creationDateTimestamp: .useExistingOrSetTo(
(dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000)
),
shouldBeVisible: .useExisting,
isDraft: .useExistingOrSetTo(true)
),
using: dependencies
)
}
self?.navigationController?.pushViewController(
ConversationVC(
threadId: memberInfo.profileId,
threadVariant: .contact,
using: dependencies
),
animated: true
)
},
using: self.viewModel.dependencies
)
) )
navigationController?.pushViewController(viewController, animated: true) navigationController?.pushViewController(viewController, animated: true)
@ -2088,23 +2067,27 @@ extension ConversationVC:
/// Trigger the deletion behaviours /// Trigger the deletion behaviours
deletionBehaviours deletionBehaviours
.publisherForAction(at: selectedIndex, using: dependencies) .publisherForAction(at: selectedIndex, using: dependencies)
.showingBlockingLoading(
in: deletionBehaviours.requiresNetworkRequestForAction(at: selectedIndex) ?
self?.viewModel.navigatableState :
nil
)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { result in receiveCompletion: { result in
switch result { DispatchQueue.main.async {
case .finished: switch result {
DispatchQueue.main.async { case .finished:
self?.viewModel.showToast( modal.dismiss(animated: true) {
text: "deleteMessageDeleted" self?.viewModel.showToast(
.putNumber(messagesToDelete.count) text: "deleteMessageDeleted"
.localized(), .putNumber(messagesToDelete.count)
backgroundColor: .backgroundSecondary, .localized(),
inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing backgroundColor: .backgroundSecondary,
) inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing
)
}
modal.close() case .failure:
}
case .failure:
DispatchQueue.main.async {
self?.viewModel.showToast( self?.viewModel.showToast(
text: "deleteMessageFailed" text: "deleteMessageFailed"
.putNumber(messagesToDelete.count) .putNumber(messagesToDelete.count)
@ -2112,7 +2095,7 @@ extension ConversationVC:
backgroundColor: .backgroundSecondary, backgroundColor: .backgroundSecondary,
inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing
) )
} }
} }
} }
) )

@ -5,6 +5,8 @@ import SessionUIKit
import SessionUtilitiesKit import SessionUtilitiesKit
public final class InputTextView: UITextView, UITextViewDelegate { public final class InputTextView: UITextView, UITextViewDelegate {
private static let defaultFont: UIFont = .systemFont(ofSize: Values.mediumFontSize)
private static let defaultThemeTextColor: ThemeValue = .textPrimary
private weak var snDelegate: InputTextViewDelegate? private weak var snDelegate: InputTextViewDelegate?
private let maxWidth: CGFloat private let maxWidth: CGFloat
private lazy var heightConstraint = self.set(.height, to: minHeight) private lazy var heightConstraint = self.set(.height, to: minHeight)
@ -69,9 +71,11 @@ public final class InputTextView: UITextView, UITextViewDelegate {
showsHorizontalScrollIndicator = false showsHorizontalScrollIndicator = false
showsVerticalScrollIndicator = false showsVerticalScrollIndicator = false
font = .systemFont(ofSize: Values.mediumFontSize) /// **Note:** If we add any additional attributes here then we will need to update the logic in
/// `textView(_:,shouldChangeTextIn:replacementText:)` to match
font = InputTextView.defaultFont
themeBackgroundColor = .clear themeBackgroundColor = .clear
themeTextColor = .textPrimary themeTextColor = InputTextView.defaultThemeTextColor
themeTintColor = .primary themeTintColor = .primary
heightConstraint.isActive = true heightConstraint.isActive = true
@ -116,7 +120,28 @@ public final class InputTextView: UITextView, UITextViewDelegate {
/// Truncate text based on character count (use `textStorage.replaceCharacters` for built in `undo` support) /// Truncate text based on character count (use `textStorage.replaceCharacters` for built in `undo` support)
let truncatedText: String = String(text.prefix(remainingSpace)) let truncatedText: String = String(text.prefix(remainingSpace))
let offset: Int = range.location + truncatedText.count let offset: Int = range.location + truncatedText.count
textView.textStorage.replaceCharacters(in: range, with: truncatedText)
/// Pasting a value that is too large into the input will result in some odd default OS styling being applied to the text which is very
/// different from our desired text style, in order to avoid this we need to detect this case and explicitly set the value as an attributed
/// string with our explicit styling
///
/// **Note:** If we add any additional attributes these will need to be updated to match
if currentText.isEmpty {
textView.textStorage.setAttributedString(
NSAttributedString(
string: truncatedText,
attributes: [
.font: textView.font ?? InputTextView.defaultFont,
.foregroundColor: textView.textColor ?? ThemeManager.currentTheme.color(
for: InputTextView.defaultThemeTextColor
) as Any
]
)
)
}
else {
textView.textStorage.replaceCharacters(in: range, with: truncatedText)
}
/// Position cursor after inserted text /// Position cursor after inserted text
/// ///

@ -929,50 +929,64 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob
) )
} }
private func viewMembers() { public static func createMemberListViewController(
self.transitionToScreen( threadId: String,
SessionTableViewController( transitionToConversation: @escaping (String) -> Void,
viewModel: UserListViewModel( using dependencies: Dependencies
title: "groupMembers".localized(), ) -> UIViewController {
showProfileIcons: true, return SessionTableViewController(
request: GroupMember viewModel: UserListViewModel(
.select( title: "groupMembers".localized(),
GroupMember.Columns.groupId, showProfileIcons: true,
GroupMember.Columns.profileId, request: GroupMember
max(GroupMember.Columns.role).forKey(GroupMember.Columns.role.name), .select(
GroupMember.Columns.roleStatus, GroupMember.Columns.groupId,
GroupMember.Columns.isHidden GroupMember.Columns.profileId,
) max(GroupMember.Columns.role).forKey(GroupMember.Columns.role.name),
.filter(GroupMember.Columns.groupId == threadId) GroupMember.Columns.roleStatus,
.group(GroupMember.Columns.profileId), GroupMember.Columns.isHidden
onTap: .callback { [weak self, dependencies] _, memberInfo in )
dependencies[singleton: .storage].write { db in .filter(GroupMember.Columns.groupId == threadId)
try SessionThread.upsert( .group(GroupMember.Columns.profileId),
db, onTap: .callback { _, memberInfo in
id: memberInfo.profileId, dependencies[singleton: .storage].write { db in
variant: .contact, try SessionThread.upsert(
values: SessionThread.TargetValues( db,
creationDateTimestamp: .useExistingOrSetTo( id: memberInfo.profileId,
dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000 variant: .contact,
), values: SessionThread.TargetValues(
shouldBeVisible: .useExisting, creationDateTimestamp: .useExistingOrSetTo(
isDraft: .useExistingOrSetTo(true) dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000
), ),
using: dependencies shouldBeVisible: .useExisting,
) isDraft: .useExistingOrSetTo(true)
}
self?.transitionToScreen(
ConversationVC(
threadId: memberInfo.profileId,
threadVariant: .contact,
using: dependencies
), ),
transitionType: .push using: dependencies
) )
}, }
using: dependencies
) transitionToConversation(memberInfo.profileId)
},
using: dependencies
)
)
}
private func viewMembers() {
self.transitionToScreen(
ThreadSettingsViewModel.createMemberListViewController(
threadId: threadId,
transitionToConversation: { [weak self, dependencies] selectedMemberId in
self?.transitionToScreen(
ConversationVC(
threadId: selectedMemberId,
threadVariant: .contact,
using: dependencies
),
transitionType: .push
)
},
using: dependencies
) )
) )
} }

@ -60,10 +60,12 @@ public struct NavigatableState {
self.showToast self.showToast
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak viewController] text, color, inset in .sink { [weak viewController] text, color, inset in
guard let view: UIView = viewController?.view else { return } guard let presenter: UIViewController = (viewController?.presentedViewController ?? viewController) else {
return
}
let toastController: ToastController = ToastController(text: text, background: color) let toastController: ToastController = ToastController(text: text, background: color)
toastController.presentToastView(fromBottomOfView: view, inset: inset) toastController.presentToastView(fromBottomOfView: presenter.view, inset: inset)
} }
.store(in: &disposables) .store(in: &disposables)
@ -113,3 +115,31 @@ public struct NavigatableState {
.store(in: &disposables) .store(in: &disposables)
} }
} }
public extension Publisher {
func showingBlockingLoading(in navigatableState: NavigatableState?) -> AnyPublisher<Output, Failure> {
guard let navigatableState: NavigatableState = navigatableState else {
return self.eraseToAnyPublisher()
}
let modalActivityIndicator: ModalActivityIndicatorViewController = ModalActivityIndicatorViewController(onAppear: { _ in })
return self
.handleEvents(
receiveSubscription: { _ in
navigatableState._transitionToScreen.send((modalActivityIndicator, .present))
}
)
.asResult()
.flatMap { result -> AnyPublisher<Output, Failure> in
Deferred {
Future<Output, Failure> { resolver in
modalActivityIndicator.dismiss(completion: {
resolver(result)
})
}
}.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}

@ -35,9 +35,16 @@ public extension MentionInfo {
let interaction: TypedTableAlias<Interaction> = TypedTableAlias() let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias() let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias()
let groupMember: TypedTableAlias<GroupMember> = TypedTableAlias() let groupMember: TypedTableAlias<GroupMember> = TypedTableAlias()
let prefixesLiteral: SQLExpression = targetPrefixes let prefixesLiteral: SQLExpression = targetPrefixes
.map { SQL("\(profile[.id]) LIKE '\(SQL(stringLiteral: "\($0.rawValue)%"))'") } .map { prefix in
SQL(
"""
(
\(profile[.id]) > '\(SQL(stringLiteral: "\(prefix.rawValue)"))' AND
\(profile[.id]) < '\(SQL(stringLiteral: "\(prefix.endOfRangeString)"))'
)
""")
}
.joined(operator: .or) .joined(operator: .or)
let profileFullTextSearch: SQL = SQL(stringLiteral: Profile.fullTextSearchTableName) let profileFullTextSearch: SQL = SQL(stringLiteral: Profile.fullTextSearchTableName)
let currentUserIds: Set<String> = [ let currentUserIds: Set<String> = [
@ -59,9 +66,10 @@ public extension MentionInfo {
return """ return """
FROM \(profileFullTextSearch) FROM \(profileFullTextSearch)
JOIN \(Profile.self) ON ( JOIN \(Profile.self) ON (
\(Profile.self).rowid = \(profileFullTextSearch).rowid AND \(Profile.self).rowid = \(profileFullTextSearch).rowid AND (
\(SQL("\(threadVariant) != \(SessionThread.Variant.community)")) OR \(SQL("\(threadVariant) != \(SessionThread.Variant.community)")) OR
\(prefixesLiteral) \(prefixesLiteral)
)
) )
""" """
}() }()
@ -118,10 +126,10 @@ public extension MentionInfo {
return SQLRequest(""" return SQLRequest("""
SELECT SELECT
\(Profile.self).*, \(Profile.self).*,
MAX(\(interaction[.timestampMs])), -- Want the newest interaction (for sorting)
\(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)")), \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)")),
\(openGroup[.server]) AS \(MentionInfo.Columns.openGroupServer), \(openGroup[.server]) AS \(MentionInfo.Columns.openGroupServer),
\(openGroup[.roomToken]) AS \(MentionInfo.Columns.openGroupRoomToken) \(openGroup[.roomToken]) AS \(MentionInfo.Columns.openGroupRoomToken),
MAX(\(interaction[.timestampMs])) -- Want the newest interaction (for sorting)
\(targetJoin) \(targetJoin)
JOIN \(Interaction.self) ON ( JOIN \(Interaction.self) ON (

@ -46,6 +46,19 @@ public extension MessageViewModel {
public let body: String public let body: String
public let actions: [NamedAction] public let actions: [NamedAction]
public func requiresNetworkRequestForAction(at index: Int) -> Bool {
guard index >= 0, index < actions.count else {
return false
}
return actions[index].behaviours.contains { behaviour in
switch behaviour {
case .preparedRequest: return true
case .cancelPendingSendJobs, .deleteFromDatabase, .markAsDeleted: return false
}
}
}
/// Collect the actions and construct a publisher which triggers each action before returning the result /// Collect the actions and construct a publisher which triggers each action before returning the result
public func publisherForAction(at index: Int, using dependencies: Dependencies) -> AnyPublisher<Void, Error> { public func publisherForAction(at index: Int, using dependencies: Dependencies) -> AnyPublisher<Void, Error> {
guard index >= 0, index < actions.count else { guard index >= 0, index < actions.count else {

@ -31,22 +31,18 @@ public final class SnodeAPI {
// If we have any config hashes to refresh TTLs then add those requests first // If we have any config hashes to refresh TTLs then add those requests first
if !refreshingConfigHashes.isEmpty { if !refreshingConfigHashes.isEmpty {
let updatedExpiryMS: Int64 = (
dependencies[cache: .snodeAPI].currentOffsetTimestampMs() +
(30 * 24 * 60 * 60 * 1000) // 30 days
)
requests.append( requests.append(
try SnodeAPI.prepareRequest( try SnodeAPI.preparedUpdateExpiry(
request: Request( serverHashes: refreshingConfigHashes,
endpoint: .expire, updatedExpiryMs: updatedExpiryMS,
swarmPublicKey: authMethod.swarmPublicKey, extendOnly: true,
body: UpdateExpiryRequest( ignoreValidationFailure: true,
messageHashes: refreshingConfigHashes, explicitTargetNode: snode,
expiryMs: UInt64( authMethod: authMethod,
dependencies[cache: .snodeAPI].currentOffsetTimestampMs() +
(30 * 24 * 60 * 60 * 1000) // 30 days
),
extend: true,
authMethod: authMethod
)
),
responseType: UpdateExpiryResponse.self,
using: dependencies using: dependencies
) )
) )
@ -78,39 +74,6 @@ public final class SnodeAPI {
let messageResponses: [Network.BatchSubResponse<PreparedGetMessagesResponse>] = batchResponse let messageResponses: [Network.BatchSubResponse<PreparedGetMessagesResponse>] = batchResponse
.compactMap { $0 as? Network.BatchSubResponse<PreparedGetMessagesResponse> } .compactMap { $0 as? Network.BatchSubResponse<PreparedGetMessagesResponse> }
/// Since we have extended the TTL for a number of messages we need to make sure we update the local
/// `SnodeReceivedMessageInfo.expirationDateMs` values so we don't end up deleting them
/// incorrectly before they actually expire on the swarm
if
!refreshingConfigHashes.isEmpty,
let refreshTTLSubReponse: Network.BatchSubResponse<UpdateExpiryResponse> = batchResponse
.first(where: { $0 is Network.BatchSubResponse<UpdateExpiryResponse> })
.asType(Network.BatchSubResponse<UpdateExpiryResponse>.self),
let refreshTTLResponse: UpdateExpiryResponse = refreshTTLSubReponse.body,
let validResults: [String: UpdateExpiryResponseResult] = try? refreshTTLResponse.validResultMap(
swarmPublicKey: authMethod.swarmPublicKey,
validationData: refreshingConfigHashes,
using: dependencies
),
let targetResult: UpdateExpiryResponseResult = validResults[snode.ed25519PubkeyHex],
let groupedExpiryResult: [UInt64: [String]] = targetResult.changed
.updated(with: targetResult.unchanged)
.groupedByValue()
.nullIfEmpty()
{
dependencies[singleton: .storage].writeAsync { db in
try groupedExpiryResult.forEach { updatedExpiry, hashes in
try SnodeReceivedMessageInfo
.filter(hashes.contains(SnodeReceivedMessageInfo.Columns.hash))
.updateAll(
db,
SnodeReceivedMessageInfo.Columns.expirationDateMs
.set(to: updatedExpiry)
)
}
}
}
return zip(namespaces, messageResponses) return zip(namespaces, messageResponses)
.reduce(into: [:]) { result, next in .reduce(into: [:]) { result, next in
guard let messageResponse: PreparedGetMessagesResponse = next.1.body else { return } guard let messageResponse: PreparedGetMessagesResponse = next.1.body else { return }
@ -404,6 +367,8 @@ public final class SnodeAPI {
updatedExpiryMs: Int64, updatedExpiryMs: Int64,
shortenOnly: Bool? = nil, shortenOnly: Bool? = nil,
extendOnly: Bool? = nil, extendOnly: Bool? = nil,
ignoreValidationFailure: Bool = false,
explicitTargetNode: LibSession.Snode? = nil,
authMethod: AuthenticationMethod, authMethod: AuthenticationMethod,
using dependencies: Dependencies using dependencies: Dependencies
) throws -> Network.PreparedRequest<[String: UpdateExpiryResponseResult]> { ) throws -> Network.PreparedRequest<[String: UpdateExpiryResponseResult]> {
@ -427,12 +392,53 @@ public final class SnodeAPI {
using: dependencies using: dependencies
) )
.tryMap { _, response -> [String: UpdateExpiryResponseResult] in .tryMap { _, response -> [String: UpdateExpiryResponseResult] in
try response.validResultMap( do {
swarmPublicKey: authMethod.swarmPublicKey, return try response.validResultMap(
validationData: serverHashes, swarmPublicKey: authMethod.swarmPublicKey,
using: dependencies validationData: serverHashes,
) using: dependencies
)
}
catch {
guard ignoreValidationFailure else { throw error }
return [:]
}
} }
.handleEvents(
receiveOutput: { _, result in
/// Since we have updated the TTL we need to make sure we also update the local
/// `SnodeReceivedMessageInfo.expirationDateMs` values so they match the updated swarm, if
/// we had a specific `snode` we we're sending the request to then we should use those values, otherwise
/// we can just grab the first value from the response and use that
let maybeTargetResult: UpdateExpiryResponseResult? = {
guard let snode: LibSession.Snode = explicitTargetNode else {
return result.first?.value
}
return result[snode.ed25519PubkeyHex]
}()
guard
let targetResult: UpdateExpiryResponseResult = maybeTargetResult,
let groupedExpiryResult: [UInt64: [String]] = targetResult.changed
.updated(with: targetResult.unchanged)
.groupedByValue()
.nullIfEmpty()
else { return }
dependencies[singleton: .storage].writeAsync { db in
try groupedExpiryResult.forEach { updatedExpiry, hashes in
try SnodeReceivedMessageInfo
.filter(hashes.contains(SnodeReceivedMessageInfo.Columns.hash))
.updateAll(
db,
SnodeReceivedMessageInfo.Columns.expirationDateMs
.set(to: updatedExpiry)
)
}
}
}
)
} }
public static func preparedRevokeSubaccounts( public static func preparedRevokeSubaccounts(

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation import Foundation
import Combine
import MediaPlayer import MediaPlayer
import SessionUIKit import SessionUIKit
import NVActivityIndicatorView import NVActivityIndicatorView
@ -202,3 +203,31 @@ public class ModalActivityIndicatorViewController: OWSViewController {
messageLabel.isHidden = (message == nil) messageLabel.isHidden = (message == nil)
} }
} }
public extension Publisher {
func showingBlockingLoading(in viewController: UIViewController?) -> AnyPublisher<Output, Failure> {
guard let viewController: UIViewController = viewController else {
return self.eraseToAnyPublisher()
}
let modalActivityIndicator: ModalActivityIndicatorViewController = ModalActivityIndicatorViewController(onAppear: { _ in })
return self
.handleEvents(
receiveSubscription: { [weak viewController] _ in
viewController?.present(modalActivityIndicator, animated: true)
}
)
.asResult()
.flatMap { result -> AnyPublisher<Output, Failure> in
Deferred {
Future<Output, Failure> { resolver in
modalActivityIndicator.dismiss(completion: {
resolver(result)
})
}
}.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}

Loading…
Cancel
Save