diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index bcf48899c..015f4439c 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -6032,7 +6032,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 389; + CURRENT_PROJECT_VERSION = 391; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6057,7 +6057,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.4; + MARKETING_VERSION = 2.2.6; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6105,7 +6105,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 389; + CURRENT_PROJECT_VERSION = 391; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6135,7 +6135,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.4; + MARKETING_VERSION = 2.2.6; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6171,7 +6171,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 389; + CURRENT_PROJECT_VERSION = 391; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6194,7 +6194,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.4; + MARKETING_VERSION = 2.2.6; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -6245,7 +6245,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 389; + CURRENT_PROJECT_VERSION = 391; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6273,7 +6273,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.4; + MARKETING_VERSION = 2.2.6; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -7173,7 +7173,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 389; + CURRENT_PROJECT_VERSION = 391; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7212,7 +7212,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.2.5; + MARKETING_VERSION = 2.2.6; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -7245,7 +7245,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 389; + CURRENT_PROJECT_VERSION = 391; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7284,7 +7284,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.2.5; + MARKETING_VERSION = 2.2.6; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index a9ccbcdf0..4c3250429 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -318,12 +318,20 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { } }() - guard shouldMarkAsRead else { return } + guard + shouldMarkAsRead, + let threadVariant: SessionThread.Variant = try? SessionThread + .filter(id: interaction.threadId) + .select(.variant) + .asRequest(of: SessionThread.Variant.self) + .fetchOne(db) + else { return } try Interaction.markAsRead( db, interactionId: interaction.id, threadId: interaction.threadId, + threadVariant: threadVariant, includingOlder: false, trySendReadReceipt: false ) diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index b8f19816b..904978ab3 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -110,6 +110,7 @@ extension ContextMenuVC { static func actions( for cellViewModel: MessageViewModel, recentEmojis: [EmojiWithSkinTones], + currentUserPublicKey: String, currentUserIsOpenGroupModerator: Bool, currentThreadIsMessageRequest: Bool, delegate: ContextMenuActionDelegate? @@ -163,6 +164,7 @@ extension ContextMenuVC { let canDelete: Bool = ( cellViewModel.threadVariant != .openGroup || currentUserIsOpenGroupModerator || + cellViewModel.authorId == currentUserPublicKey || cellViewModel.state == .failed ) let canBan: Bool = ( diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 7758c398e..c6794329a 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -777,6 +777,7 @@ extension ConversationVC: let actions: [ContextMenuVC.Action] = ContextMenuVC.actions( for: cellViewModel, recentEmojis: (self.viewModel.threadData.recentReactionEmoji ?? []).compactMap { EmojiWithSkinTones(rawValue: $0) }, + currentUserPublicKey: self.viewModel.threadData.currentUserPublicKey, currentUserIsOpenGroupModerator: OpenGroupManager.isUserModeratorOrAdmin( self.viewModel.threadData.currentUserPublicKey, for: self.viewModel.threadData.openGroupRoomToken, diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index e906db92d..f4e2adf2a 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -737,11 +737,17 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl // Store the 'sentMessageBeforeUpdate' state locally let didSendMessageBeforeUpdate: Bool = self.viewModel.sentMessageBeforeUpdate + let wasOnlyUpdates: Bool = ( + changeset.count == 1 && + changeset[0].elementUpdated.count == changeset[0].changeCount + ) self.viewModel.sentMessageBeforeUpdate = false - // When sending a message we want to reload the UI instantly (with any form of animation the message - // sending feels somewhat unresponsive but an instant update feels snappy) - guard !didSendMessageBeforeUpdate else { + // When sending a message, or if there were only cell updates (ie. read status changes) we want to + // reload the UI instantly (with any form of animation the message sending feels somewhat unresponsive + // but an instant update feels snappy and without the instant update there is some overlap of the read + // status text change even though there shouldn't be any animations) + guard !didSendMessageBeforeUpdate && !wasOnlyUpdates else { self.viewModel.updateInteractionData(updatedData) self.tableView.reloadData() diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index cdb434a40..92e55e45b 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -197,7 +197,17 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { return SQL("LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId])") }() - ) + ), + PagedData.ObservedChanges( + table: RecipientState.self, + columns: [.state, .mostRecentFailureText], + joinToPagedType: { + let interaction: TypedTableAlias = TypedTableAlias() + let recipientState: TypedTableAlias = TypedTableAlias() + + return SQL("LEFT JOIN \(RecipientState.self) ON \(recipientState[.interactionId]) = \(interaction[.id])") + }() + ), ], filterSQL: MessageViewModel.filterSQL(threadId: threadId), groupSQL: MessageViewModel.groupSQL, @@ -405,6 +415,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { else { return } let threadId: String = self.threadData.threadId + let threadVariant: SessionThread.Variant = self.threadData.threadVariant let trySendReadReceipt: Bool = (self.threadData.threadIsMessageRequest == false) self.lastInteractionIdMarkedAsRead = targetInteractionId @@ -413,6 +424,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { db, interactionId: targetInteractionId, threadId: threadId, + threadVariant: threadVariant, includingOlder: true, trySendReadReceipt: trySendReadReceipt ) diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 3a9d4d33f..6ac622f60 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -165,6 +165,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { return result }() + + internal lazy var messageStatusLabelPaddingView: UIView = UIView() // MARK: - Settings @@ -252,6 +254,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { underBubbleStackView.addArrangedSubview(reactionContainerView) underBubbleStackView.addArrangedSubview(messageStatusContainerView) + underBubbleStackView.addArrangedSubview(messageStatusLabelPaddingView) messageStatusContainerView.addSubview(messageStatusLabel) messageStatusContainerView.addSubview(messageStatusImageView) @@ -267,6 +270,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { messageStatusLabel.center(.vertical, in: messageStatusContainerView) messageStatusLabel.pin(.leading, to: .leading, of: messageStatusContainerView) messageStatusLabel.pin(.trailing, to: .leading, of: messageStatusImageView, withInset: -2) + messageStatusLabelPaddingView.pin(.leading, to: .leading, of: messageStatusContainerView) + messageStatusLabelPaddingView.pin(.trailing, to: .trailing, of: messageStatusContainerView) } override func setUpGestureRecognizers() { @@ -429,6 +434,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { !cellViewModel.isLast ) ) + messageStatusLabelPaddingView.isHidden = ( + messageStatusContainerView.isHidden || + cellViewModel.isLast + ) // Set the height of the underBubbleStackView to 0 if it has no content (need to do this // otherwise it can randomly stretch) @@ -1121,11 +1130,15 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { return [:] } + // Note: The 'String.count' value is based on actual character counts whereas + // NSAttributedString and NSRange are both based on UTF-16 encoded lengths, so + // in order to avoid strings which contain emojis breaking strings which end + // with URLs we need to use the 'String.utf16.count' value when creating the range return detector .matches( in: attributedText.string, options: [], - range: NSRange(location: 0, length: attributedText.string.count) + range: NSRange(location: 0, length: attributedText.string.utf16.count) ) .reduce(into: [:]) { result, match in guard diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 1d2111f94..7667d2806 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -546,6 +546,7 @@ class NotificationActionHandler { db, interactionId: interaction.id, threadId: thread.id, + threadVariant: thread.variant, includingOlder: true, trySendReadReceipt: true ) @@ -600,6 +601,7 @@ class NotificationActionHandler { .asRequest(of: Int64.self) .fetchOne(db), threadId: thread.id, + threadVariant: thread.variant, includingOlder: true, trySendReadReceipt: true ) diff --git a/Session/Path/PathStatusView.swift b/Session/Path/PathStatusView.swift index 41b578024..0f8326a1e 100644 --- a/Session/Path/PathStatusView.swift +++ b/Session/Path/PathStatusView.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit +import Reachability import SessionUIKit final class PathStatusView: UIView { @@ -42,6 +43,7 @@ final class PathStatusView: UIView { // MARK: - Initialization private let size: Size + private let reachability: Reachability = Reachability.forInternetConnection() init(size: Size = .small) { self.size = size @@ -73,15 +75,34 @@ final class PathStatusView: UIView { self.set(.width, to: self.size.pointSize) self.set(.height, to: self.size.pointSize) - setStatus(to: (!OnionRequestAPI.paths.isEmpty ? .connected : .connecting)) + switch (reachability.isReachable(), OnionRequestAPI.paths.isEmpty) { + case (false, _): setStatus(to: .error) + case (true, true): setStatus(to: .connecting) + case (true, false): setStatus(to: .connected) + } } // MARK: - Functions private func registerObservers() { - let notificationCenter = NotificationCenter.default - notificationCenter.addObserver(self, selector: #selector(handleBuildingPathsNotification), name: .buildingPaths, object: nil) - notificationCenter.addObserver(self, selector: #selector(handlePathsBuiltNotification), name: .pathsBuilt, object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(handleBuildingPathsNotification), + name: .buildingPaths, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(handlePathsBuiltNotification), + name: .pathsBuilt, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(reachabilityChanged), + name: .reachabilityChanged, + object: nil + ) } private func setStatus(to status: Status) { @@ -102,10 +123,34 @@ final class PathStatusView: UIView { } @objc private func handleBuildingPathsNotification() { + guard reachability.isReachable() else { + setStatus(to: .error) + return + } + setStatus(to: .connecting) } @objc private func handlePathsBuiltNotification() { + guard reachability.isReachable() else { + setStatus(to: .error) + return + } + setStatus(to: .connected) } + + @objc private func reachabilityChanged() { + guard Thread.isMainThread else { + DispatchQueue.main.async { [weak self] in self?.reachabilityChanged() } + return + } + + guard reachability.isReachable() else { + setStatus(to: .error) + return + } + + setStatus(to: (!OnionRequestAPI.paths.isEmpty ? .connected : .connecting)) + } } diff --git a/Session/Settings/PrivacySettingsViewModel.swift b/Session/Settings/PrivacySettingsViewModel.swift index 2564ca7ab..b7a279b5e 100644 --- a/Session/Settings/PrivacySettingsViewModel.swift +++ b/Session/Settings/PrivacySettingsViewModel.swift @@ -3,6 +3,7 @@ import Foundation import Combine import GRDB +import LocalAuthentication import DifferenceKit import SessionUIKit import SessionMessagingKit @@ -99,7 +100,23 @@ class PrivacySettingsViewModel: SessionTableViewModel Job? { guard db[.areReadReceiptsEnabled] == true else { return nil } // Retrieve the timestampMs values for the specified interactions - let maybeTimestampMsValues: [Int64]? = try? Int64.fetchAll( - db, - Interaction - .select(.timestampMs) - .filter(interactionIds.contains(Interaction.Columns.id)) - // Only `standardIncoming` incoming interactions should have read receipts sent - .filter(Interaction.Columns.variant == Interaction.Variant.standardIncoming) - .filter(Interaction.Columns.wasRead == false) // Only send for unread messages - .joining( - // Don't send read receipts in group threads - required: Interaction.thread - .filter(SessionThread.Columns.variant != SessionThread.Variant.closedGroup) - .filter(SessionThread.Columns.variant != SessionThread.Variant.openGroup) - ) - .distinct() - ) + let timestampMsValues: [Int64] = (try? Interaction + .select(.timestampMs) + .filter(interactionIds.contains(Interaction.Columns.id)) + .distinct() + .asRequest(of: Int64.self) + .fetchAll(db)) + .defaulting(to: []) // If there are no timestamp values then do nothing - guard - let timestampMsValues: [Int64] = maybeTimestampMsValues, - !timestampMsValues.isEmpty - else { return nil } + guard !timestampMsValues.isEmpty else { return nil } // Try to get an existing job (if there is one that's not running) if diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift index 28d9517d7..e2cc442ea 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift @@ -19,7 +19,12 @@ extension MessageReceiver { guard let interactionId: Int64 = maybeInteraction?.id, - let interaction: Interaction = maybeInteraction + let interaction: Interaction = maybeInteraction, + let threadVariant: SessionThread.Variant = try SessionThread + .filter(id: interaction.threadId) + .select(.variant) + .asRequest(of: SessionThread.Variant.self) + .fetchOne(db) else { return } // Mark incoming messages as read and remove any of their notifications @@ -28,6 +33,7 @@ extension MessageReceiver { db, interactionId: interactionId, threadId: interaction.threadId, + threadVariant: threadVariant, includingOlder: false, trySendReadReceipt: false ) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index 12e15fc9c..73006ac67 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -405,6 +405,7 @@ extension MessageReceiver { db, interactionId: interactionId, threadId: thread.id, + threadVariant: thread.variant, includingOlder: true, trySendReadReceipt: true )