Fixed issues raised during QA

Fixed a bug where the legacy group invitation was getting sent to the wrong location
Fixed a bug where outgoing typing indicators would be sent to blocked contacts
Fixed a bug where the call button was visible for blocked contacts
Fixed a bug where read receipts could be sent to blocked contacts
Fixed a bug where the conversation nav buttons wouldn't get updated correctly in some cases
Fixed a bug where we could incorrectly include the current user in the contacts syncing
Fixed a bug where the initial state of the Note to Self conversation wasn't getting synced
Fixed a bug where the Note to Self conversation could get removed
Fixed a bug with where the conversation title would be misaligned in some cases
Fixed a bug where link previews and quotes with images weren't getting sent correctly
Fixed a crash when removing a user from a legacy group
Added some missing accessibility info
Updated the code to ensure the user is kicked from the conversation if it's deletion gets synced while it's open
Updated the conversation empty state copy
This commit is contained in:
Morgan Pretty 2023-03-17 15:12:35 +11:00
parent 1c7eaed8b6
commit 5fdfd6df3b
48 changed files with 463 additions and 81 deletions

View File

@ -818,6 +818,7 @@
FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */; };
FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; };
FDDC08F229A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */; };
FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */; };
FDE658A129418C7900A33BC1 /* CryptoKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE658A029418C7900A33BC1 /* CryptoKit+Utilities.swift */; };
FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE658A229418E2F00A33BC1 /* KeyPair.swift */; };
FDE77F6B280FEB28002CFC5D /* ControlMessageProcessRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */; };
@ -1946,6 +1947,7 @@
FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageCollectionJob.swift; sourceTree = "<group>"; };
FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = "<group>"; };
FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionTypeConversionUtilitiesSpec.swift; sourceTree = "<group>"; };
FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FetchRequest+Utilities.swift"; sourceTree = "<group>"; };
FDE658A029418C7900A33BC1 /* CryptoKit+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CryptoKit+Utilities.swift"; sourceTree = "<group>"; };
FDE658A229418E2F00A33BC1 /* KeyPair.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyPair.swift; sourceTree = "<group>"; };
FDE7214F287E50D50093DF33 /* ProtoWrappers.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ProtoWrappers.py; sourceTree = "<group>"; };
@ -3718,6 +3720,7 @@
FD17D7C227F5204C00122BE0 /* Database+Utilities.swift */,
FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */,
FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */,
FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */,
FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */,
FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */,
);
@ -5642,6 +5645,7 @@
FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */,
FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */,
FDF8487A29405906007DCAE5 /* HTTPError.swift in Sources */,
FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */,
B87EF18126377A1D00124B3C /* Features.swift in Sources */,
FD09797727FAB7A600936362 /* Data+Image.swift in Sources */,
C300A60D2554B31900555489 /* Logging.swift in Sources */,

View File

@ -64,6 +64,7 @@ extension ConversationVC:
@objc func startCall(_ sender: Any?) {
guard SessionCall.isEnabled else { return }
guard viewModel.threadData.threadIsBlocked == false else { return }
guard Storage.shared[.areCallsEnabled] else {
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
@ -678,9 +679,11 @@ extension ConversationVC:
let threadId: String = self.viewModel.threadData.threadId
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
let threadIsMessageRequest: Bool = (self.viewModel.threadData.threadIsMessageRequest == true)
let threadIsBlocked: Bool = (self.viewModel.threadData.threadIsBlocked == true)
let needsToStartTypingIndicator: Bool = TypingIndicators.didStartTypingNeedsToStart(
threadId: threadId,
threadVariant: threadVariant,
threadIsBlocked: threadIsBlocked,
threadIsMessageRequest: threadIsMessageRequest,
direction: .outgoing,
timestampMs: SnodeAPI.currentOffsetTimestampMs()

View File

@ -9,7 +9,7 @@ import SessionMessagingKit
import SessionUtilitiesKit
import SignalUtilitiesKit
final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITableViewDataSource, UITableViewDelegate {
final class ConversationVC: BaseVC, SessionUtilRespondingViewController, ConversationSearchControllerDelegate, UITableViewDataSource, UITableViewDelegate {
private static let loadingHeaderHeight: CGFloat = 40
internal let viewModel: ConversationViewModel
@ -211,8 +211,14 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
private lazy var emptyStateLabel: UILabel = {
let text: String = String(
format: "CONVERSATION_EMPTY_STATE".localized(),
self.viewModel.threadData.displayName
format: {
switch (viewModel.threadData.threadIsNoteToSelf, viewModel.threadData.canWrite) {
case (true, _): return "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF".localized()
case (_, false): return "CONVERSATION_EMPTY_STATE_READ_ONLY".localized()
default: return "CONVERSATION_EMPTY_STATE".localized()
}
}(),
viewModel.threadData.displayName
)
let result: UILabel = UILabel()
@ -385,8 +391,16 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
// nav will be offset incorrectly during the push animation (unfortunately the profile icon still
// doesn't appear until after the animation, I assume it's taking a snapshot or something, but
// there isn't much we can do about that unfortunately)
updateNavBarButtons(threadData: nil, initialVariant: self.viewModel.initialThreadVariant)
titleView.initialSetup(with: self.viewModel.initialThreadVariant)
updateNavBarButtons(
threadData: nil,
initialVariant: self.viewModel.initialThreadVariant,
initialIsNoteToSelf: self.viewModel.threadData.threadIsNoteToSelf,
initialIsBlocked: (self.viewModel.threadData.threadIsBlocked == true)
)
titleView.initialSetup(
with: self.viewModel.initialThreadVariant,
isNoteToSelf: self.viewModel.threadData.threadIsNoteToSelf
)
// Constraints
view.addSubview(tableView)
@ -533,6 +547,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
let threadId: String = viewModel.threadData.threadId
if
viewModel.threadData.threadIsNoteToSelf == false &&
viewModel.threadData.threadShouldBeVisible == false &&
!SessionUtil.conversationExistsInConfig(
threadId: threadId,
@ -674,9 +689,16 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
// Update the empty state
let text: String = String(
format: "CONVERSATION_EMPTY_STATE".localized(),
format: {
switch (updatedThreadData.threadIsNoteToSelf, updatedThreadData.canWrite) {
case (true, _): return "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF".localized()
case (_, false): return "CONVERSATION_EMPTY_STATE_READ_ONLY".localized()
default: return "CONVERSATION_EMPTY_STATE".localized()
}
}(),
updatedThreadData.displayName
)
emptyStateLabel.attributedText = NSAttributedString(string: text)
.adding(
attributes: [.font: UIFont.boldSystemFont(ofSize: Values.verySmallFontSize)],
@ -689,11 +711,17 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
if
initialLoad ||
viewModel.threadData.threadVariant != updatedThreadData.threadVariant ||
viewModel.threadData.threadIsBlocked != updatedThreadData.threadIsBlocked ||
viewModel.threadData.threadRequiresApproval != updatedThreadData.threadRequiresApproval ||
viewModel.threadData.threadIsMessageRequest != updatedThreadData.threadIsMessageRequest ||
viewModel.threadData.profile != updatedThreadData.profile
{
updateNavBarButtons(threadData: updatedThreadData, initialVariant: viewModel.initialThreadVariant)
updateNavBarButtons(
threadData: updatedThreadData,
initialVariant: viewModel.initialThreadVariant,
initialIsNoteToSelf: viewModel.threadData.threadIsNoteToSelf,
initialIsBlocked: (viewModel.threadData.threadIsBlocked == true)
)
let messageRequestsViewWasVisible: Bool = (
messageRequestStackView.isHidden == false
@ -1139,7 +1167,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
}
}
func updateNavBarButtons(threadData: SessionThreadViewModel?, initialVariant: SessionThread.Variant) {
func updateNavBarButtons(
threadData: SessionThreadViewModel?,
initialVariant: SessionThread.Variant,
initialIsNoteToSelf: Bool,
initialIsBlocked: Bool
) {
navigationItem.hidesBackButton = isShowingSearchUI
if isShowingSearchUI {
@ -1147,6 +1180,13 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
navigationItem.rightBarButtonItems = []
}
else {
let shouldHaveCallButton: Bool = (
SessionCall.isEnabled &&
(threadData?.threadVariant ?? initialVariant) == .contact &&
(threadData?.threadIsNoteToSelf ?? initialIsNoteToSelf) == false &&
(threadData?.threadIsBlocked ?? initialIsBlocked) == false
)
guard
let threadData: SessionThreadViewModel = threadData,
(
@ -1169,7 +1209,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
)
)
),
(initialVariant == .contact ?
(shouldHaveCallButton ?
UIBarButtonItem(customView: UIView(frame: CGRect(x: 0, y: 0, width: 44, height: 44))) :
nil
)
@ -1199,7 +1239,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
settingsButtonItem.accessibilityLabel = "More options"
settingsButtonItem.isAccessibilityElement = true
if SessionCall.isEnabled && !threadData.threadIsNoteToSelf {
if shouldHaveCallButton {
let callButton = UIBarButtonItem(
image: UIImage(named: "Phone"),
style: .plain,
@ -1207,11 +1247,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
action: #selector(startCall)
)
callButton.accessibilityLabel = "Call button"
callButton.isAccessibilityElement = true
navigationItem.rightBarButtonItems = [settingsButtonItem, callButton]
}
else {
navigationItem.rightBarButtonItem = settingsButtonItem
navigationItem.rightBarButtonItems = [settingsButtonItem]
}
default:
@ -1640,7 +1681,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
}
// Nav bar buttons
updateNavBarButtons(threadData: self.viewModel.threadData, initialVariant: viewModel.initialThreadVariant)
updateNavBarButtons(
threadData: viewModel.threadData,
initialVariant: viewModel.initialThreadVariant,
initialIsNoteToSelf: viewModel.threadData.threadIsNoteToSelf,
initialIsBlocked: (viewModel.threadData.threadIsBlocked == true)
)
// Hack so that the ResultsBar stays on the screen when dismissing the search field
// keyboard.
@ -1675,7 +1721,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
@objc func hideSearchUI() {
isShowingSearchUI = false
navigationItem.titleView = titleView
updateNavBarButtons(threadData: self.viewModel.threadData, initialVariant: viewModel.initialThreadVariant)
updateNavBarButtons(
threadData: viewModel.threadData,
initialVariant: viewModel.initialThreadVariant,
initialIsNoteToSelf: viewModel.threadData.threadIsNoteToSelf,
initialIsBlocked: (viewModel.threadData.threadIsBlocked == true)
)
searchController.uiSearchController.stubbableSearchBar.stubbedNextResponder = nil
becomeFirstResponder()
@ -1800,4 +1851,10 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
.highlight()
}
}
// MARK: - SessionUtilRespondingViewController
func isConversation(in threadIds: [String]) -> Bool {
return threadIds.contains(self.viewModel.threadData.threadId)
}
}

View File

@ -111,6 +111,16 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
threadId: self.threadId,
threadVariant: self.initialThreadVariant,
threadIsNoteToSelf: (self.threadId == getUserHexEncodedPublicKey()),
threadIsBlocked: (self.initialThreadVariant != .contact ? false :
Storage.shared.read { db in
try Contact
.filter(id: self.threadId)
.select(.isBlocked)
.asRequest(of: Bool.self)
.fetchOne(db)
.defaulting(to: false)
}
),
currentUserIsClosedGroupMember: ((self.initialThreadVariant != .legacyGroup && self.initialThreadVariant != .group) ?
nil :
Storage.shared.read { db in
@ -120,6 +130,15 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
.filter(GroupMember.Columns.role == GroupMember.Role.standard)
.isNotEmpty(db)
}
),
openGroupPermissions: (self.initialThreadVariant != .community ? nil :
Storage.shared.read { db in
try OpenGroup
.filter(id: threadId)
.select(.permissions)
.asRequest(of: OpenGroup.Permissions.self)
.fetchOne(db)
}
)
)
.populatingCurrentUserBlindedKey()

View File

@ -93,7 +93,6 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
let result: UIView = UIView()
result.accessibilityLabel = "Mentions list"
result.accessibilityIdentifier = "Mentions list"
result.isAccessibilityElement = true
result.alpha = 0
let backgroundView = UIView()

View File

@ -92,6 +92,11 @@ final class MentionSelectionView: UIView, UITableViewDataSource, UITableViewDele
),
isLast: (indexPath.row == (candidates.count - 1))
)
cell.accessibilityIdentifier = "Contact"
cell.accessibilityLabel = candidates[indexPath.row].profile.displayName(
for: candidates[indexPath.row].threadVariant
)
cell.isAccessibilityElement = true
return cell
}

View File

@ -71,10 +71,13 @@ final class ConversationTitleView: UIView {
// MARK: - Content
public func initialSetup(with threadVariant: SessionThread.Variant) {
public func initialSetup(
with threadVariant: SessionThread.Variant,
isNoteToSelf: Bool
) {
self.update(
with: " ",
isNoteToSelf: false,
isNoteToSelf: isNoteToSelf,
threadVariant: threadVariant,
mutedUntilTimestamp: nil,
onlyNotifyForMentions: false,

View File

@ -8,7 +8,7 @@ import SessionMessagingKit
import SessionUtilitiesKit
import SignalUtilitiesKit
final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedReminderViewDelegate {
final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewDataSource, UITableViewDelegate, SeedReminderViewDelegate {
private static let loadingHeaderHeight: CGFloat = 40
public static let newConversationButtonSize: CGFloat = 60
@ -20,6 +20,10 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
private var isAutoLoadingNextPage: Bool = false
private var viewHasAppeared: Bool = false
// MARK: - SessionUtilRespondingViewController
let isConversationList: Bool = true
// MARK: - Intialization
init() {

View File

@ -481,19 +481,10 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
title: "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON".localized(),
style: .destructive
) { _ in
// Clear the requests
Storage.shared.write { db in
_ = try SessionThread
.filter(ids: contactThreadIds)
.deleteAll(db)
try ClosedGroup.removeKeysAndUnsubscribe(
db,
threadIds: closedGroupThreadIds,
removeGroupData: true,
calledFromConfigHandling: false
)
}
MessageRequestsViewModel.clearAllRequests(
contactThreadIds: contactThreadIds,
closedGroupThreadIds: closedGroupThreadIds
)
})
alertVC.addAction(UIAlertAction(title: "TXT_CANCEL_TITLE".localized(), style: .cancel, handler: nil))

View File

@ -186,7 +186,10 @@ public class MessageRequestsViewModel {
) { _ in
Storage.shared.write { db in
switch threadVariant {
case .contact, .community:
case .contact:
try SessionUtil
.hide(db, contactIds: [threadId])
_ = try SessionThread
.filter(id: threadId)
.deleteAll(db)
@ -201,6 +204,8 @@ public class MessageRequestsViewModel {
// Trigger a config sync
ConfigurationSyncJob.enqueue(db, publicKey: getUserHexEncodedPublicKey(db))
default: break
}
}
@ -245,6 +250,10 @@ public class MessageRequestsViewModel {
Contact.Columns.didApproveMe.set(to: true)
)
// Sync the removal of the thread from other devices
try SessionUtil
.hide(db, contactIds: [threadId])
// Remove the thread
_ = try SessionThread
.filter(id: threadId)
@ -257,4 +266,29 @@ public class MessageRequestsViewModel {
viewController?.present(modal, animated: true, completion: nil)
}
static func clearAllRequests(
contactThreadIds: [String],
closedGroupThreadIds: [String]
) {
// Clear the requests
Storage.shared.write { db in
// Sync the removal of the thread from other devices
try SessionUtil
.hide(db, contactIds: contactThreadIds)
// Remove the threads
_ = try SessionThread
.filter(ids: contactThreadIds)
.deleteAll(db)
// Remove the groups
try ClosedGroup.removeKeysAndUnsubscribe(
db,
threadIds: closedGroupThreadIds,
removeGroupData: true,
calledFromConfigHandling: false
)
}
}
}

View File

@ -533,7 +533,8 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
let cell: PhotoGridViewCell = collectionView.dequeue(type: PhotoGridViewCell.self, for: indexPath)
let assetItem = photoCollectionContents.assetItem(at: indexPath.item, photoMediaSize: photoMediaSize)
cell.configure(item: assetItem)
cell.isAccessibilityElement = true
cell.accessibilityIdentifier = "\(assetItem.asset.modificationDate.map { "\($0)" } ?? "Unknown Date")"
cell.isSelected = delegate.imagePicker(self, isAssetSelected: assetItem.asset)
return cell

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -614,4 +614,7 @@
"MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread";
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"UPDATE_PROFILE_TITLE" = "Update Profile Picture";

View File

@ -517,13 +517,13 @@ class NotificationActionHandler {
return Fail(error: NotificationError.failDebug("threadId was unexpectedly nil"))
.eraseToAnyPublisher()
}
guard let thread: SessionThread = Storage.shared.read({ db in try SessionThread.fetchOne(db, id: threadId) }) else {
guard Storage.shared.read({ db in try SessionThread.exists(db, id: threadId) }) == true else {
return Fail(error: NotificationError.failDebug("unable to find thread with id: \(threadId)"))
.eraseToAnyPublisher()
}
return markAsRead(thread: thread)
return markAsRead(threadId: threadId)
}
func reply(userInfo: [AnyHashable: Any], replyText: String) -> AnyPublisher<Void, Error> {
@ -540,7 +540,7 @@ class NotificationActionHandler {
return Storage.shared
.writePublisher(receiveOn: DispatchQueue.main) { db in
let interaction: Interaction = try Interaction(
threadId: thread.id,
threadId: threadId,
authorId: getUserHexEncodedPublicKey(db),
variant: .standardOutgoing,
body: replyText,
@ -557,16 +557,20 @@ class NotificationActionHandler {
try Interaction.markAsRead(
db,
interactionId: interaction.id,
threadId: thread.id,
threadId: threadId,
threadVariant: thread.variant,
includingOlder: true,
trySendReadReceipt: true
trySendReadReceipt: try SessionThread.canSendReadReceipt(
db,
threadId: threadId,
threadVariant: thread.variant
)
)
return try MessageSender.preparedSendData(
db,
interaction: interaction,
threadId: thread.id,
threadId: threadId,
threadVariant: thread.variant
)
}
@ -605,20 +609,34 @@ class NotificationActionHandler {
.eraseToAnyPublisher()
}
private func markAsRead(thread: SessionThread) -> AnyPublisher<Void, Error> {
private func markAsRead(threadId: String) -> AnyPublisher<Void, Error> {
return Storage.shared
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
try Interaction.markAsRead(
db,
interactionId: try thread.interactions
guard
let threadVariant: SessionThread.Variant = try SessionThread
.filter(id: threadId)
.select(.variant)
.asRequest(of: SessionThread.Variant.self)
.fetchOne(db),
let lastInteractionId: Int64 = try Interaction
.select(.id)
.filter(Interaction.Columns.threadId == threadId)
.order(Interaction.Columns.timestampMs.desc)
.asRequest(of: Int64.self)
.fetchOne(db),
threadId: thread.id,
threadVariant: thread.variant,
.fetchOne(db)
else { throw NotificationError.failDebug("unable to required thread info: \(threadId)") }
try Interaction.markAsRead(
db,
interactionId: lastInteractionId,
threadId: threadId,
threadVariant: threadVariant,
includingOlder: true,
trySendReadReceipt: true
trySendReadReceipt: try SessionThread.canSendReadReceipt(
db,
threadId: threadId,
threadVariant: threadVariant
)
)
}
.eraseToAnyPublisher()

View File

@ -487,7 +487,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
private func updateProfilePicture(hasCustomImage: Bool) {
let actionSheet: UIAlertController = UIAlertController(
title: "Update Profile Picture",
title: "UPDATE_PROFILE_TITLE".localized(),
message: nil,
preferredStyle: .actionSheet
)

View File

@ -39,6 +39,12 @@ enum _014_GenerateInitialUserConfigDumps: Migration {
in: conf
)
try SessionUtil.updateNoteToSelf(
hidden: (allThreads[userPublicKey]?.shouldBeVisible == true),
priority: Int32(allThreads[userPublicKey]?.pinnedPriority ?? 0),
in: conf
)
if config_needs_dump(conf) {
try SessionUtil
.createDump(
@ -55,11 +61,12 @@ enum _014_GenerateInitialUserConfigDumps: Migration {
try SessionUtil
.config(for: .contacts, publicKey: userPublicKey)
.mutate { conf in
// Exclude community, group and outgoing blinded message requests
// Exclude Note to Self, community, group and outgoing blinded message requests
let validContactIds: [String] = allThreads
.values
.filter { thread in
thread.variant == .contact &&
thread.id != userPublicKey &&
SessionId(from: thread.id)?.prefix == .standard
}
.map { $0.id }

View File

@ -206,6 +206,48 @@ public extension SessionThread {
)
}
static func canSendReadReceipt(
_ db: Database,
threadId: String,
threadVariant maybeThreadVariant: SessionThread.Variant? = nil,
isBlocked maybeIsBlocked: Bool? = nil,
isMessageRequest maybeIsMessageRequest: Bool? = nil
) throws -> Bool {
let threadVariant: SessionThread.Variant = try {
try maybeThreadVariant ??
SessionThread
.filter(id: threadId)
.select(.variant)
.asRequest(of: SessionThread.Variant.self)
.fetchOne(db, orThrow: StorageError.objectNotFound)
}()
let threadIsBlocked: Bool = try {
try maybeIsBlocked ??
(
threadVariant == .contact &&
Contact
.filter(id: threadId)
.select(.isBlocked)
.asRequest(of: Bool.self)
.fetchOne(db, orThrow: StorageError.objectNotFound)
)
}()
let threadIsMessageRequest: Bool = SessionThread
.filter(id: threadId)
.filter(
SessionThread.isMessageRequest(
userPublicKey: getUserHexEncodedPublicKey(db),
includeNonVisible: true
)
)
.isNotEmpty(db)
return (
!threadIsBlocked &&
!threadIsMessageRequest
)
}
@available(*, unavailable, message: "should not be used until pin re-ordering is built")
static func refreshPinnedPriorities(_ db: Database, adding threadId: String) throws {
struct PinnedPriority: TableRecord, ColumnExpressible {

View File

@ -168,6 +168,8 @@ internal extension SessionUtil {
switch (data.shouldBeVisible, threadExists) {
case (false, true):
SessionUtil.kickFromConversationUIIfNeeded(removedThreadIds: [contact.id])
try SessionThread
.filter(id: contact.id)
.deleteAll(db)
@ -212,6 +214,8 @@ internal extension SessionUtil {
.fetchAll(db)
if !contactIdsToRemove.isEmpty {
SessionUtil.kickFromConversationUIIfNeeded(removedThreadIds: contactIdsToRemove)
try Contact
.filter(ids: contactIdsToRemove)
.deleteAll(db)
@ -224,6 +228,11 @@ internal extension SessionUtil {
Profile.Columns.nickname.set(to: nil)
)
// Delete the one-to-one conversations associated to the contact
try SessionThread
.filter(ids: contactIdsToRemove)
.deleteAll(db)
try SessionUtil.remove(db, volatileContactIds: contactIdsToRemove)
}
}

View File

@ -1,7 +1,8 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import UIKit
import GRDB
import SessionUIKit
import SessionUtil
import SessionUtilitiesKit
@ -191,6 +192,44 @@ internal extension SessionUtil {
return updated
}
static func kickFromConversationUIIfNeeded(removedThreadIds: [String]) {
guard !removedThreadIds.isEmpty else { return }
// If the user is currently navigating somewhere within the view hierarchy of a conversation
// we just deleted then return to the home screen
DispatchQueue.main.async {
guard
let rootViewController: UIViewController = CurrentAppContext().mainWindow?.rootViewController,
let topBannerController: TopBannerController = (rootViewController as? TopBannerController),
!topBannerController.children.isEmpty,
let navController: UINavigationController = topBannerController.children[0] as? UINavigationController
else { return }
// Extract the ones which will respond to SessionUtil changes
let targetViewControllers: [any SessionUtilRespondingViewController] = navController
.viewControllers
.compactMap({ $0 as? SessionUtilRespondingViewController })
// Make sure we have a conversation list and that one of the removed conversations are
// in the nav hierarchy
guard
targetViewControllers.count > 1,
targetViewControllers.contains(where: { $0.isConversationList }),
targetViewControllers.contains(where: { $0.isConversation(in: removedThreadIds) })
else { return }
// Return to the root view controller as the removed conversation will be invalid
if navController.presentedViewController != nil {
navController.dismiss(animated: false) {
navController.popToRootViewController(animated: true)
}
}
else {
navController.popToRootViewController(animated: true)
}
}
}
}
// MARK: - External Outgoing Changes
@ -285,3 +324,17 @@ extension SessionUtil {
let shouldBeVisible: Bool
}
}
// MARK: - SessionUtilRespondingViewController
public protocol SessionUtilRespondingViewController {
var isConversationList: Bool { get }
func isConversation(in threadIds: [String]) -> Bool
}
public extension SessionUtilRespondingViewController {
var isConversationList: Bool { false }
func isConversation(in threadIds: [String]) -> Bool { return false }
}

View File

@ -168,12 +168,16 @@ internal extension SessionUtil {
.keys)
.subtracting(communities.map { $0.data.threadId })
communityIdsToRemove.forEach { threadId in
OpenGroupManager.shared.delete(
db,
openGroupId: threadId,
calledFromConfigHandling: true
)
if !communityIdsToRemove.isEmpty {
SessionUtil.kickFromConversationUIIfNeeded(removedThreadIds: Array(communityIdsToRemove))
communityIdsToRemove.forEach { threadId in
OpenGroupManager.shared.delete(
db,
openGroupId: threadId,
calledFromConfigHandling: true
)
}
}
// MARK: -- Handle Legacy Group Changes
@ -320,6 +324,8 @@ internal extension SessionUtil {
.subtracting(legacyGroups.map { $0.id })
if !legacyGroupIdsToRemove.isEmpty {
SessionUtil.kickFromConversationUIIfNeeded(removedThreadIds: Array(legacyGroupIdsToRemove))
try ClosedGroup.removeKeysAndUnsubscribe(
db,
threadIds: Array(legacyGroupIdsToRemove),

View File

@ -11,24 +11,40 @@ extension MessageReceiver {
threadVariant: SessionThread.Variant,
message: TypingIndicator
) throws {
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else { return }
guard try SessionThread.exists(db, id: threadId) else { return }
switch message.kind {
case .started:
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let threadIsBlocked: Bool = (
threadVariant == .contact &&
(try? Contact
.filter(id: threadId)
.select(.isBlocked)
.asRequest(of: Bool.self)
.fetchOne(db))
.defaulting(to: false)
)
let threadIsMessageRequest: Bool = (try? SessionThread
.filter(id: threadId)
.filter(SessionThread.isMessageRequest(userPublicKey: userPublicKey, includeNonVisible: true))
.isEmpty(db))
.defaulting(to: false)
let needsToStartTypingIndicator: Bool = TypingIndicators.didStartTypingNeedsToStart(
threadId: threadId,
threadVariant: threadVariant,
threadIsMessageRequest: thread.isMessageRequest(db),
threadIsBlocked: threadIsBlocked,
threadIsMessageRequest: threadIsMessageRequest,
direction: .incoming,
timestampMs: message.sentTimestamp.map { Int64($0) }
)
if needsToStartTypingIndicator {
TypingIndicators.start(db, threadId: thread.id, direction: .incoming)
TypingIndicators.start(db, threadId: threadId, direction: .incoming)
}
case .stopped:
TypingIndicators.didStopTyping(db, threadId: thread.id, direction: .incoming)
TypingIndicators.didStopTyping(db, threadId: threadId, direction: .incoming)
default:
SNLog("Unknown TypingIndicator Kind ignored")

View File

@ -196,7 +196,7 @@ extension MessageReceiver {
// If we receive an outgoing message that already exists in the database
// then we still need up update the recipient and read states for the
// message (even if we don't need to do anything else)
try updateRecipientAndReadStates(
try updateRecipientAndReadStatesForOutgoingInteraction(
db,
thread: thread,
interactionId: existingInteractionId,
@ -214,7 +214,7 @@ extension MessageReceiver {
guard let interactionId: Int64 = interaction.id else { throw StorageError.failedToSave }
// Update and recipient and read states as needed
try updateRecipientAndReadStates(
try updateRecipientAndReadStatesForOutgoingInteraction(
db,
thread: thread,
interactionId: interactionId,
@ -398,7 +398,7 @@ extension MessageReceiver {
return interactionId
}
private static func updateRecipientAndReadStates(
private static func updateRecipientAndReadStatesForOutgoingInteraction(
_ db: Database,
thread: SessionThread,
interactionId: Int64,
@ -454,7 +454,7 @@ extension MessageReceiver {
threadId: thread.id,
threadVariant: thread.variant,
includingOlder: true,
trySendReadReceipt: true
trySendReadReceipt: false
)
// Process any PendingReadReceipt values

View File

@ -455,8 +455,8 @@ extension MessageSender {
)
),
interactionId: nil,
threadId: closedGroup.threadId,
threadVariant: .legacyGroup
threadId: member,
threadVariant: .contact
)
// Add the users to the group
@ -548,13 +548,16 @@ extension MessageSender {
)
)
.flatMap { _ -> AnyPublisher<Void, Error> in
generateAndSendNewEncryptionKeyPair(
db,
targetMembers: members,
userPublicKey: userPublicKey,
allGroupMembers: allGroupMembers,
closedGroup: closedGroup
)
Storage.shared
.writePublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
generateAndSendNewEncryptionKeyPair(
db,
targetMembers: members,
userPublicKey: userPublicKey,
allGroupMembers: allGroupMembers,
closedGroup: closedGroup
)
}
}
.eraseToAnyPublisher()
}

View File

@ -599,7 +599,13 @@ public final class MessageSender {
// `MessageSender.performUploadsIfNeeded(queue:preparedSendData:)` before calling this function
switch preparedSendData.message {
case let visibleMessage as VisibleMessage:
guard visibleMessage.attachmentIds.count == preparedSendData.totalAttachmentsUploaded else {
let expectedAttachmentUploadCount: Int = (
visibleMessage.attachmentIds.count +
(visibleMessage.linkPreview?.attachmentId != nil ? 1 : 0) +
(visibleMessage.quote?.attachmentId != nil ? 1 : 0)
)
guard expectedAttachmentUploadCount == preparedSendData.totalAttachmentsUploaded else {
return Fail(error: MessageSenderError.attachmentsNotUploaded)
.eraseToAnyPublisher()
}

View File

@ -25,6 +25,7 @@ public class TypingIndicators {
init?(
threadId: String,
threadVariant: SessionThread.Variant,
threadIsBlocked: Bool,
threadIsMessageRequest: Bool,
direction: Direction,
timestampMs: Int64?
@ -34,9 +35,11 @@ public class TypingIndicators {
// or show typing indicators for other users
//
// We also don't want to show/send typing indicators for message requests
guard Storage.shared[.typingIndicatorsEnabled] && !threadIsMessageRequest else {
return nil
}
guard
Storage.shared[.typingIndicatorsEnabled] &&
!threadIsBlocked &&
!threadIsMessageRequest
else { return nil }
// Don't send typing indicators in group threads
guard
@ -143,6 +146,7 @@ public class TypingIndicators {
public static func didStartTypingNeedsToStart(
threadId: String,
threadVariant: SessionThread.Variant,
threadIsBlocked: Bool,
threadIsMessageRequest: Bool,
direction: Direction,
timestampMs: Int64?
@ -159,6 +163,7 @@ public class TypingIndicators {
let newIndicator: Indicator? = Indicator(
threadId: threadId,
threadVariant: threadVariant,
threadIsBlocked: threadIsBlocked,
threadIsMessageRequest: threadIsMessageRequest,
direction: direction,
timestampMs: timestampMs
@ -179,6 +184,7 @@ public class TypingIndicators {
let newIndicator: Indicator? = Indicator(
threadId: threadId,
threadVariant: threadVariant,
threadIsBlocked: threadIsBlocked,
threadIsMessageRequest: threadIsMessageRequest,
direction: direction,
timestampMs: timestampMs

View File

@ -279,7 +279,8 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
let threadId: String = self.threadId
let threadVariant: SessionThread.Variant = self.threadVariant
let trySendReadReceipt: Bool = (self.threadIsMessageRequest == false)
let threadIsBlocked: Bool? = self.threadIsBlocked
let threadIsMessageRequest: Bool? = self.threadIsMessageRequest
Storage.shared.writeAsync { db in
// Only make this change if needed (want to avoid triggering a thread update
@ -299,7 +300,13 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
threadId: threadId,
threadVariant: threadVariant,
includingOlder: true,
trySendReadReceipt: trySendReadReceipt
trySendReadReceipt: try SessionThread.canSendReadReceipt(
db,
threadId: threadId,
threadVariant: threadVariant,
isBlocked: threadIsBlocked,
isMessageRequest: threadIsMessageRequest
)
)
}
}
@ -332,8 +339,10 @@ public extension SessionThreadViewModel {
threadId: String? = nil,
threadVariant: SessionThread.Variant? = nil,
threadIsNoteToSelf: Bool = false,
threadIsBlocked: Bool? = nil,
contactProfile: Profile? = nil,
currentUserIsClosedGroupMember: Bool? = nil,
openGroupPermissions: OpenGroup.Permissions? = nil,
unreadCount: UInt = 0
) {
self.rowId = -1
@ -347,7 +356,7 @@ public extension SessionThreadViewModel {
self.threadRequiresApproval = false
self.threadShouldBeVisible = false
self.threadPinnedPriority = 0
self.threadIsBlocked = nil
self.threadIsBlocked = threadIsBlocked
self.threadMutedUntilTimestamp = nil
self.threadOnlyNotifyForMentions = nil
self.threadMessageDraft = nil
@ -373,7 +382,7 @@ public extension SessionThreadViewModel {
self.openGroupPublicKey = nil
self.openGroupProfilePictureData = nil
self.openGroupUserCount = nil
self.openGroupPermissions = nil
self.openGroupPermissions = openGroupPermissions
// Interaction display info

View File

@ -0,0 +1,11 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import GRDB
public extension FetchRequest where RowDecoder: DatabaseValueConvertible {
func fetchOne(_ db: Database, orThrow error: Error) throws -> RowDecoder {
guard let result: RowDecoder = try fetchOne(db) else { throw error }
return result
}
}

View File

@ -14,6 +14,16 @@ public extension UIViewController {
var nextViewController: UIViewController? = viewController.presentedViewController
if
let topBannerController: TopBannerController = nextViewController as? TopBannerController,
!topBannerController.children.isEmpty
{
nextViewController = (
topBannerController.children[0].presentedViewController ??
topBannerController.children[0]
)
}
if let nextViewController: UIViewController = nextViewController {
if !ignoringAlerts || !(nextViewController is UIAlertController) {
if visitedViewControllers.contains(nextViewController) {