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:
parent
1c7eaed8b6
commit
5fdfd6df3b
|
@ -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 */,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue