Finished off a few remaining bits and pieces
Added the 'outdated client' warning banner Added a unit test to validate the 'group(by:)' method maintains ordering Added an error when trying to message a non-standard session id directly Removed the "hide" logic for groups (don't want it) Removed some unneeded thread fetching Updated the logic to use the 'lastHash' when fetching config messages Updated the logic to use the libSession value restrictions instead of hard-coded values Fixed an issue where members weren't getting removed from legacy groups
This commit is contained in:
parent
972519d7d9
commit
66fd2d4ff8
|
@ -449,7 +449,6 @@
|
|||
C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A74C2553A39700C340D1 /* VisibleMessage.swift */; };
|
||||
C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A75E2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift */; };
|
||||
C3C2A7852553AAF300C340D1 /* SessionProtos.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A7832553AAF300C340D1 /* SessionProtos.pb.swift */; };
|
||||
C3C2ABD22553C6C900C340D1 /* Data+SecureRandom.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2ABD12553C6C900C340D1 /* Data+SecureRandom.swift */; };
|
||||
C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2AC2D2553CBEB00C340D1 /* String+Trimming.swift */; };
|
||||
C3CA3AA2255CDADA00F4C6D4 /* english.txt in Resources */ = {isa = PBXBuildFile; fileRef = C3CA3AA1255CDADA00F4C6D4 /* english.txt */; };
|
||||
C3CA3AB4255CDAE600F4C6D4 /* japanese.txt in Resources */ = {isa = PBXBuildFile; fileRef = C3CA3AB3255CDAE600F4C6D4 /* japanese.txt */; };
|
||||
|
@ -522,6 +521,8 @@
|
|||
FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E728264937000CE219 /* MediaDetailViewController.swift */; };
|
||||
FD09C5EA282A1BB2000CE219 /* ThreadTypingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E9282A1BB2000CE219 /* ThreadTypingIndicator.swift */; };
|
||||
FD09C5EC282B8F18000CE219 /* AttachmentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5EB282B8F17000CE219 /* AttachmentError.swift */; };
|
||||
FD0B77B029B69A65009169BA /* TopBannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0B77AF29B69A65009169BA /* TopBannerController.swift */; };
|
||||
FD0B77B229B82B7A009169BA /* ArrayUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0B77B129B82B7A009169BA /* ArrayUtilitiesSpec.swift */; };
|
||||
FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */; };
|
||||
FD17D79C27F40B2E00122BE0 /* SMKLegacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79B27F40B2E00122BE0 /* SMKLegacy.swift */; };
|
||||
FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */; };
|
||||
|
@ -737,7 +738,7 @@
|
|||
FD87DD0428B8727D00AF0F98 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DD0328B8727D00AF0F98 /* Configuration.swift */; };
|
||||
FD8ECF7929340F7200C0D1BB /* libsession-util.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = FD8ECF7829340F7100C0D1BB /* libsession-util.xcframework */; };
|
||||
FD8ECF7B29340FFD00C0D1BB /* SessionUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7A29340FFD00C0D1BB /* SessionUtil.swift */; };
|
||||
FD8ECF7D2934293A00C0D1BB /* _012_SharedUtilChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7C2934293A00C0D1BB /* _012_SharedUtilChanges.swift */; };
|
||||
FD8ECF7D2934293A00C0D1BB /* _012_SessionUtilChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7C2934293A00C0D1BB /* _012_SessionUtilChanges.swift */; };
|
||||
FD8ECF7F2934298100C0D1BB /* SharedConfigDump.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7E2934298100C0D1BB /* SharedConfigDump.swift */; };
|
||||
FD8ECF822934387A00C0D1BB /* ConfigUserProfileSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF812934387A00C0D1BB /* ConfigUserProfileSpec.swift */; };
|
||||
FD8ECF892935AB7200C0D1BB /* SessionUtilError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF882935AB7200C0D1BB /* SessionUtilError.swift */; };
|
||||
|
@ -1622,7 +1623,6 @@
|
|||
C3C2A7702553A41E00C340D1 /* ControlMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlMessage.swift; sourceTree = "<group>"; };
|
||||
C3C2A7822553AAF200C340D1 /* SNProto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SNProto.swift; sourceTree = "<group>"; };
|
||||
C3C2A7832553AAF300C340D1 /* SessionProtos.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionProtos.pb.swift; sourceTree = "<group>"; };
|
||||
C3C2ABD12553C6C900C340D1 /* Data+SecureRandom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+SecureRandom.swift"; sourceTree = "<group>"; };
|
||||
C3C2AC2D2553CBEB00C340D1 /* String+Trimming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Trimming.swift"; sourceTree = "<group>"; };
|
||||
C3C3CF8824D8EED300E1CCE7 /* TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = "<group>"; };
|
||||
C3CA3AA1255CDADA00F4C6D4 /* english.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = english.txt; sourceTree = "<group>"; };
|
||||
|
@ -1693,6 +1693,8 @@
|
|||
FD09C5E728264937000CE219 /* MediaDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDetailViewController.swift; sourceTree = "<group>"; };
|
||||
FD09C5E9282A1BB2000CE219 /* ThreadTypingIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTypingIndicator.swift; sourceTree = "<group>"; };
|
||||
FD09C5EB282B8F17000CE219 /* AttachmentError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentError.swift; sourceTree = "<group>"; };
|
||||
FD0B77AF29B69A65009169BA /* TopBannerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopBannerController.swift; sourceTree = "<group>"; };
|
||||
FD0B77B129B82B7A009169BA /* ArrayUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayUtilitiesSpec.swift; sourceTree = "<group>"; };
|
||||
FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = "<group>"; };
|
||||
FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = "<group>"; };
|
||||
FD17D79B27F40B2E00122BE0 /* SMKLegacy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMKLegacy.swift; sourceTree = "<group>"; };
|
||||
|
@ -1867,7 +1869,7 @@
|
|||
FD87DD0328B8727D00AF0F98 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
|
||||
FD8ECF7829340F7100C0D1BB /* libsession-util.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = "libsession-util.xcframework"; sourceTree = "<group>"; };
|
||||
FD8ECF7A29340FFD00C0D1BB /* SessionUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionUtil.swift; sourceTree = "<group>"; };
|
||||
FD8ECF7C2934293A00C0D1BB /* _012_SharedUtilChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _012_SharedUtilChanges.swift; sourceTree = "<group>"; };
|
||||
FD8ECF7C2934293A00C0D1BB /* _012_SessionUtilChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _012_SessionUtilChanges.swift; sourceTree = "<group>"; };
|
||||
FD8ECF7E2934298100C0D1BB /* SharedConfigDump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedConfigDump.swift; sourceTree = "<group>"; };
|
||||
FD8ECF812934387A00C0D1BB /* ConfigUserProfileSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigUserProfileSpec.swift; sourceTree = "<group>"; };
|
||||
FD8ECF882935AB7200C0D1BB /* SessionUtilError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionUtilError.swift; sourceTree = "<group>"; };
|
||||
|
@ -2541,7 +2543,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
FDE658A029418C7900A33BC1 /* CryptoKit+Utilities.swift */,
|
||||
C3C2ABD12553C6C900C340D1 /* Data+SecureRandom.swift */,
|
||||
B88FA7FA26114EA70049422F /* Hex.swift */,
|
||||
FDE658A229418E2F00A33BC1 /* KeyPair.swift */,
|
||||
C3A71F882558BA9F0043A11F /* Mnemonic.swift */,
|
||||
|
@ -2928,6 +2929,7 @@
|
|||
B86BD08323399ACF000F5AE3 /* Modal.swift */,
|
||||
FD52090628B49738006098F6 /* ConfirmationModal.swift */,
|
||||
FD71165A28E6DDBC00B47552 /* StyledNavigationController.swift */,
|
||||
FD0B77AF29B69A65009169BA /* TopBannerController.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3630,7 +3632,7 @@
|
|||
7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */,
|
||||
FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */,
|
||||
FD432431299C6933008A0213 /* _011_AddPendingReadReceipts.swift */,
|
||||
FD8ECF7C2934293A00C0D1BB /* _012_SharedUtilChanges.swift */,
|
||||
FD8ECF7C2934293A00C0D1BB /* _012_SessionUtilChanges.swift */,
|
||||
FD778B6329B189FF001BAC6B /* _013_GenerateInitialUserConfigDumps.swift */,
|
||||
);
|
||||
path = Migrations;
|
||||
|
@ -4013,6 +4015,7 @@
|
|||
FD83B9B927CF20A5005E1583 /* General */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FD0B77B129B82B7A009169BA /* ArrayUtilitiesSpec.swift */,
|
||||
FD83B9BA27CF20AF005E1583 /* SessionIdSpec.swift */,
|
||||
);
|
||||
path = General;
|
||||
|
@ -5363,6 +5366,7 @@
|
|||
FD37EA0128A60473003AE748 /* UIKit+Theme.swift in Sources */,
|
||||
FD37E9CF28A1EB1B003AE748 /* Theme.swift in Sources */,
|
||||
C331FFB92558FA8D00070591 /* UIView+Constraints.swift in Sources */,
|
||||
FD0B77B029B69A65009169BA /* TopBannerController.swift in Sources */,
|
||||
FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */,
|
||||
FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */,
|
||||
C331FFE02558FB0000070591 /* SearchBar.swift in Sources */,
|
||||
|
@ -5555,7 +5559,6 @@
|
|||
FDBB25E32988B13800F1508E /* _004_AddJobPriority.swift in Sources */,
|
||||
C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */,
|
||||
7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */,
|
||||
C3C2ABD22553C6C900C340D1 /* Data+SecureRandom.swift in Sources */,
|
||||
FD848B8B283DC509000E298B /* PagedDatabaseObserver.swift in Sources */,
|
||||
B8856E09256F1676001CE70E /* UIDevice+featureSupport.swift in Sources */,
|
||||
B8856DEF256F161F001CE70E /* NSString+SSK.m in Sources */,
|
||||
|
@ -5749,7 +5752,7 @@
|
|||
FD43EE9F297E2EE0009C87C5 /* SessionUtil+ConvoInfoVolatile.swift in Sources */,
|
||||
B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */,
|
||||
FDF8487F29405994007DCAE5 /* HTTPHeader+OpenGroup.swift in Sources */,
|
||||
FD8ECF7D2934293A00C0D1BB /* _012_SharedUtilChanges.swift in Sources */,
|
||||
FD8ECF7D2934293A00C0D1BB /* _012_SessionUtilChanges.swift in Sources */,
|
||||
FD17D7A227F40F0500122BE0 /* _001_InitialSetupMigration.swift in Sources */,
|
||||
FD245C5D2850660F00B966DD /* OWSAudioPlayer.m in Sources */,
|
||||
FDF0B7582807F368004C14C5 /* MessageReceiverError.swift in Sources */,
|
||||
|
@ -6073,6 +6076,7 @@
|
|||
FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */,
|
||||
FD9B30F3293EA0BF008DEE3E /* BatchResponseSpec.swift in Sources */,
|
||||
FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */,
|
||||
FD0B77B229B82B7A009169BA /* ArrayUtilitiesSpec.swift in Sources */,
|
||||
FD1A94FE2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift in Sources */,
|
||||
FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */,
|
||||
);
|
||||
|
|
|
@ -346,7 +346,7 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
guard !updatedName.isEmpty else {
|
||||
return showError(title: "vc_create_closed_group_group_name_missing_error".localized())
|
||||
}
|
||||
guard updatedName.count < 64 else {
|
||||
guard updatedName.utf8CString.count < SessionUtil.libSessionMaxGroupNameByteLength else {
|
||||
return showError(title: "vc_create_closed_group_group_name_too_long_error".localized())
|
||||
}
|
||||
|
||||
|
|
|
@ -320,7 +320,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
|
|||
else {
|
||||
return showError(title: "vc_create_closed_group_group_name_missing_error".localized())
|
||||
}
|
||||
guard name.count < 30 else {
|
||||
guard name.utf8CString.count < SessionUtil.libSessionMaxGroupNameByteLength else {
|
||||
return showError(title: "vc_create_closed_group_group_name_too_long_error".localized())
|
||||
}
|
||||
guard selectedContacts.count >= 1 else {
|
||||
|
|
|
@ -431,6 +431,7 @@ extension ConversationVC:
|
|||
// use it to determine if the user is creating a new thread and update the 'isApproved'
|
||||
// flags appropriately
|
||||
let threadId: String = self.viewModel.threadData.threadId
|
||||
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
|
||||
let oldThreadShouldBeVisible: Bool = (self.viewModel.threadData.threadShouldBeVisible == true)
|
||||
let sentTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs()
|
||||
let linkPreviewDraft: LinkPreviewDraft? = snInputView.linkPreviewInfo?.draft
|
||||
|
@ -447,15 +448,11 @@ extension ConversationVC:
|
|||
// Send the message
|
||||
Storage.shared
|
||||
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { [weak self] db in
|
||||
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Let the viewModel know we are about to send a message
|
||||
self?.viewModel.sentMessageBeforeUpdate = true
|
||||
|
||||
// Update the thread to be visible (if it isn't already)
|
||||
if !thread.shouldBeVisible {
|
||||
if self?.viewModel.threadData.threadShouldBeVisible == false {
|
||||
_ = try SessionThread
|
||||
.filter(id: threadId)
|
||||
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||
|
@ -515,7 +512,8 @@ extension ConversationVC:
|
|||
try MessageSender.send(
|
||||
db,
|
||||
interaction: interaction,
|
||||
in: thread
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant
|
||||
)
|
||||
}
|
||||
.sinkUntilComplete(
|
||||
|
@ -566,6 +564,7 @@ extension ConversationVC:
|
|||
// use it to determine if the user is creating a new thread and update the 'isApproved'
|
||||
// flags appropriately
|
||||
let threadId: String = self.viewModel.threadData.threadId
|
||||
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
|
||||
let oldThreadShouldBeVisible: Bool = (self.viewModel.threadData.threadShouldBeVisible == true)
|
||||
let sentTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs()
|
||||
|
||||
|
@ -580,15 +579,11 @@ extension ConversationVC:
|
|||
// Send the message
|
||||
Storage.shared
|
||||
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { [weak self] db in
|
||||
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Let the viewModel know we are about to send a message
|
||||
self?.viewModel.sentMessageBeforeUpdate = true
|
||||
|
||||
// Update the thread to be visible (if it isn't already)
|
||||
if !thread.shouldBeVisible {
|
||||
if self?.viewModel.threadData.threadShouldBeVisible == false {
|
||||
_ = try SessionThread
|
||||
.filter(id: threadId)
|
||||
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||
|
@ -621,13 +616,13 @@ extension ConversationVC:
|
|||
for: interactionId
|
||||
)
|
||||
|
||||
// Prepare the message send data
|
||||
try MessageSender
|
||||
.send(
|
||||
db,
|
||||
interaction: interaction,
|
||||
in: thread
|
||||
)
|
||||
// Send the message
|
||||
try MessageSender.send(
|
||||
db,
|
||||
interaction: interaction,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant
|
||||
)
|
||||
}
|
||||
.sinkUntilComplete(
|
||||
receiveCompletion: { [weak self] _ in
|
||||
|
@ -1308,17 +1303,11 @@ extension ConversationVC:
|
|||
|
||||
// Perform the sending logic
|
||||
Storage.shared
|
||||
.writePublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db -> AnyPublisher<MessageSender.PreparedSendData?, Error> in
|
||||
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: cellViewModel.threadId) else {
|
||||
return Just(nil)
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
.writePublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { [weak self] db -> AnyPublisher<MessageSender.PreparedSendData?, Error> in
|
||||
// Update the thread to be visible (if it isn't already)
|
||||
if !thread.shouldBeVisible {
|
||||
if self?.viewModel.threadData.threadShouldBeVisible == false {
|
||||
_ = try SessionThread
|
||||
.filter(id: thread.id)
|
||||
.filter(id: cellViewModel.threadId)
|
||||
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||
}
|
||||
|
||||
|
@ -1386,8 +1375,11 @@ extension ConversationVC:
|
|||
kind: (remove ? .remove : .react)
|
||||
)
|
||||
),
|
||||
to: try Message.Destination.from(db, thread: thread),
|
||||
namespace: try Message.Destination.from(db, thread: thread).defaultNamespace,
|
||||
to: try Message.Destination
|
||||
.from(db, threadId: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant),
|
||||
namespace: try Message.Destination
|
||||
.from(db, threadId: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant)
|
||||
.defaultNamespace,
|
||||
interactionId: cellViewModel.id
|
||||
)
|
||||
|
||||
|
@ -1640,8 +1632,8 @@ extension ConversationVC:
|
|||
Storage.shared.writeAsync { [weak self] db in
|
||||
guard
|
||||
let threadId: String = self?.viewModel.threadData.threadId,
|
||||
let interaction: Interaction = try? Interaction.fetchOne(db, id: cellViewModel.id),
|
||||
let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId)
|
||||
let threadVariant: SessionThread.Variant = self?.viewModel.threadData.threadVariant,
|
||||
let interaction: Interaction = try? Interaction.fetchOne(db, id: cellViewModel.id)
|
||||
else { return }
|
||||
|
||||
if
|
||||
|
@ -1664,7 +1656,8 @@ extension ConversationVC:
|
|||
try MessageSender.send(
|
||||
db,
|
||||
interaction: interaction,
|
||||
in: thread,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
isSyncMessage: (cellViewModel.state == .failedToSync)
|
||||
)
|
||||
}
|
||||
|
@ -1749,7 +1742,6 @@ extension ConversationVC:
|
|||
case .standardOutgoing, .standardIncoming: break
|
||||
}
|
||||
|
||||
let threadId: String = self.viewModel.threadData.threadId
|
||||
let threadName: String = self.viewModel.threadData.displayName
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey()
|
||||
|
||||
|
@ -1800,7 +1792,7 @@ extension ConversationVC:
|
|||
.filter(id: cellViewModel.id)
|
||||
.asRequest(of: Int64.self)
|
||||
.fetchOne(db),
|
||||
try OpenGroup.fetchOne(db, id: threadId)
|
||||
try OpenGroup.fetchOne(db, id: cellViewModel.threadId)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1915,7 +1907,7 @@ extension ConversationVC:
|
|||
.send(
|
||||
db,
|
||||
message: unsendRequest,
|
||||
threadId: threadId,
|
||||
threadId: cellViewModel.threadId,
|
||||
interactionId: nil,
|
||||
to: .contact(publicKey: userPublicKey)
|
||||
)
|
||||
|
@ -1938,7 +1930,7 @@ extension ConversationVC:
|
|||
.send(
|
||||
db,
|
||||
message: unsendRequest,
|
||||
threadId: threadId,
|
||||
threadId: cellViewModel.threadId,
|
||||
interactionId: nil,
|
||||
to: .contact(publicKey: userPublicKey)
|
||||
)
|
||||
|
@ -1964,23 +1956,20 @@ extension ConversationVC:
|
|||
from: self,
|
||||
request: SnodeAPI
|
||||
.deleteMessages(
|
||||
publicKey: threadId,
|
||||
publicKey: cellViewModel.threadId,
|
||||
serverHashes: [serverHash]
|
||||
)
|
||||
.map { _ in () }
|
||||
.eraseToAnyPublisher()
|
||||
) { [weak self] in
|
||||
Storage.shared.writeAsync { db in
|
||||
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
|
||||
return
|
||||
}
|
||||
|
||||
try MessageSender
|
||||
.send(
|
||||
db,
|
||||
message: unsendRequest,
|
||||
interactionId: nil,
|
||||
in: thread
|
||||
threadId: cellViewModel.threadId,
|
||||
threadVariant: cellViewModel.threadVariant
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -2042,17 +2031,17 @@ extension ConversationVC:
|
|||
}
|
||||
|
||||
let threadId: String = self.viewModel.threadData.threadId
|
||||
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
|
||||
|
||||
Storage.shared.writeAsync { db in
|
||||
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else { return }
|
||||
|
||||
try MessageSender.send(
|
||||
db,
|
||||
message: DataExtractionNotification(
|
||||
kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs))
|
||||
),
|
||||
interactionId: nil,
|
||||
in: thread
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -2310,17 +2299,17 @@ extension ConversationVC:
|
|||
guard self.viewModel.threadData.threadVariant == .contact else { return }
|
||||
|
||||
let threadId: String = self.viewModel.threadData.threadId
|
||||
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
|
||||
|
||||
Storage.shared.writeAsync { db in
|
||||
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else { return }
|
||||
|
||||
try MessageSender.send(
|
||||
db,
|
||||
message: DataExtractionNotification(
|
||||
kind: .screenshot
|
||||
),
|
||||
interactionId: nil,
|
||||
in: thread
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -2380,17 +2369,9 @@ extension ConversationVC {
|
|||
// (it'll be updated with correct profile info if they accept the message request so this
|
||||
// shouldn't cause weird behaviours)
|
||||
guard
|
||||
let approvalData: (contact: Contact, thread: SessionThread?) = Storage.shared.read({ db in
|
||||
return (
|
||||
Contact.fetchOrCreate(db, id: threadId),
|
||||
try SessionThread.fetchOne(db, id: threadId)
|
||||
)
|
||||
}),
|
||||
let thread: SessionThread = approvalData.thread,
|
||||
!approvalData.contact.isApproved
|
||||
else {
|
||||
return
|
||||
}
|
||||
let contact: Contact = Storage.shared.read({ db in Contact.fetchOrCreate(db, id: threadId) }),
|
||||
!contact.isApproved
|
||||
else { return }
|
||||
|
||||
Storage.shared
|
||||
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
|
||||
|
@ -2405,19 +2386,20 @@ extension ConversationVC {
|
|||
sentTimestampMs: UInt64(timestampMs)
|
||||
),
|
||||
interactionId: nil,
|
||||
in: thread
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant
|
||||
)
|
||||
}
|
||||
|
||||
// Default 'didApproveMe' to true for the person approving the message request
|
||||
try approvalData.contact.save(db)
|
||||
try contact.save(db)
|
||||
try Contact
|
||||
.filter(id: approvalData.contact.id)
|
||||
.filter(id: contact.id)
|
||||
.updateAllAndConfig(
|
||||
db,
|
||||
Contact.Columns.isApproved.set(to: true),
|
||||
Contact.Columns.didApproveMe
|
||||
.set(to: approvalData.contact.didApproveMe || !isNewThread)
|
||||
.set(to: contact.didApproveMe || !isNewThread)
|
||||
)
|
||||
}
|
||||
.sinkUntilComplete(
|
||||
|
|
|
@ -148,10 +148,6 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
|
|||
guard self.config != updatedConfig else { return }
|
||||
|
||||
dependencies.storage.writeAsync { db in
|
||||
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
|
||||
return
|
||||
}
|
||||
|
||||
let config: DisappearingMessagesConfiguration = try DisappearingMessagesConfiguration
|
||||
.fetchOne(db, id: threadId)
|
||||
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(threadId))
|
||||
|
@ -177,7 +173,8 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
|
|||
duration: UInt32(floor(updatedConfig.isEnabled ? updatedConfig.durationSeconds : 0))
|
||||
),
|
||||
interactionId: interaction.id,
|
||||
in: thread
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant
|
||||
)
|
||||
|
||||
// Legacy closed groups
|
||||
|
|
|
@ -750,7 +750,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
try MessageSender.send(
|
||||
db,
|
||||
interaction: interaction,
|
||||
in: thread
|
||||
threadId: thread.id,
|
||||
threadVariant: thread.variant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -632,50 +632,20 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
|||
|
||||
func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let section: HomeViewModel.SectionModel = self.viewModel.threadData[indexPath.section]
|
||||
let unswipeAnimationDelay: DispatchTimeInterval = .milliseconds(500)
|
||||
|
||||
switch section.model {
|
||||
case .threads:
|
||||
let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row]
|
||||
let isUnread: Bool = (
|
||||
threadViewModel.threadWasMarkedUnread == true ||
|
||||
(threadViewModel.threadUnreadCount ?? 0) > 0
|
||||
)
|
||||
let changeReadStatus: UIContextualAction = UIContextualAction(
|
||||
title: (isUnread ?
|
||||
"MARK_AS_READ".localized() :
|
||||
"MARK_AS_UNREAD".localized()
|
||||
),
|
||||
icon: (isUnread ?
|
||||
UIImage(systemName: "envelope.open") :
|
||||
UIImage(systemName: "envelope.badge")
|
||||
),
|
||||
themeTintColor: .textPrimary,
|
||||
themeBackgroundColor: .conversationButton_swipeRead,
|
||||
side: .leading,
|
||||
actionIndex: 0,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { [weak self] _, _, completionHandler in
|
||||
// Delay the change to give the cell "unswipe" animation some time to complete
|
||||
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) {
|
||||
switch isUnread {
|
||||
case true:
|
||||
self?.viewModel.markAsRead(
|
||||
threadViewModel: threadViewModel,
|
||||
target: .threadAndInteractions(
|
||||
interactionsBeforeInclusive: threadViewModel.interactionId
|
||||
)
|
||||
)
|
||||
|
||||
case false:
|
||||
self?.viewModel.markAsUnread(threadViewModel: threadViewModel)
|
||||
}
|
||||
}
|
||||
completionHandler(true)
|
||||
// Cannot properly sync outgoing blinded message requests so don't provide the option
|
||||
guard SessionId(from: section.elements[indexPath.row].threadId)?.prefix == .standard else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return UISwipeActionsConfiguration(actions: [changeReadStatus])
|
||||
return generateSwipeActions(
|
||||
[.toggleReadStatus],
|
||||
for: .leading,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
)
|
||||
|
||||
default: return nil
|
||||
}
|
||||
|
@ -683,152 +653,255 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
|||
|
||||
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let section: HomeViewModel.SectionModel = self.viewModel.threadData[indexPath.section]
|
||||
let unswipeAnimationDelay: DispatchTimeInterval = .milliseconds(500)
|
||||
|
||||
switch section.model {
|
||||
case .messageRequests:
|
||||
let hide: UIContextualAction = UIContextualAction(
|
||||
title: "TXT_HIDE_TITLE".localized(),
|
||||
icon: UIImage(systemName: "eye.slash"),
|
||||
themeTintColor: .textPrimary,
|
||||
themeBackgroundColor: .conversationButton_swipeDestructive,
|
||||
side: .trailing,
|
||||
actionIndex: 0,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { _, _, completionHandler in
|
||||
Storage.shared.write { db in db[.hasHiddenMessageRequests] = true }
|
||||
completionHandler(true)
|
||||
}
|
||||
|
||||
return UISwipeActionsConfiguration(actions: [hide])
|
||||
return generateSwipeActions([.hide], for: .trailing, indexPath: indexPath, tableView: tableView)
|
||||
|
||||
case .threads:
|
||||
let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row]
|
||||
let sessionIdPrefix: SessionId.Prefix? = SessionId(from: threadViewModel.threadId)?.prefix
|
||||
|
||||
// Cannot properly sync outgoing blinded message requests and can only block contact
|
||||
// threads so only provide these options if valid
|
||||
let shouldHaveBlockAction: Bool = (
|
||||
threadViewModel.threadVariant == .contact &&
|
||||
!threadViewModel.threadIsNoteToSelf
|
||||
!threadViewModel.threadIsNoteToSelf &&
|
||||
sessionIdPrefix != .blinded
|
||||
)
|
||||
let delete: UIContextualAction = UIContextualAction(
|
||||
title: "TXT_DELETE_TITLE".localized(),
|
||||
icon: UIImage(named: "icon_bin"),
|
||||
themeTintColor: .textPrimary,
|
||||
themeBackgroundColor: .conversationButton_swipeDestructive,
|
||||
side: .trailing,
|
||||
actionIndex: 2,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { [weak self] _, _, completionHandler in
|
||||
let confirmationModal: ConfirmationModal = ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
title: "CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE".localized(),
|
||||
explanation: (threadViewModel.currentUserIsClosedGroupAdmin == true ?
|
||||
"admin_group_leave_warning".localized() :
|
||||
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE".localized()
|
||||
),
|
||||
confirmTitle: "TXT_DELETE_TITLE".localized(),
|
||||
confirmStyle: .danger,
|
||||
cancelStyle: .alert_text,
|
||||
dismissOnConfirm: true,
|
||||
onConfirm: { [weak self] _ in
|
||||
self?.viewModel.deleteOrLeave(
|
||||
threadId: threadViewModel.threadId,
|
||||
threadVariant: threadViewModel.threadVariant
|
||||
)
|
||||
self?.dismiss(animated: true, completion: nil)
|
||||
|
||||
completionHandler(true)
|
||||
},
|
||||
afterClosed: { completionHandler(false) }
|
||||
)
|
||||
)
|
||||
|
||||
self?.present(confirmationModal, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
let pin: UIContextualAction = UIContextualAction(
|
||||
title: (threadViewModel.threadPinnedPriority > 0 ?
|
||||
"UNPIN_BUTTON_TEXT".localized() :
|
||||
"PIN_BUTTON_TEXT".localized()
|
||||
),
|
||||
icon: (threadViewModel.threadPinnedPriority > 0 ?
|
||||
UIImage(systemName: "pin.slash") :
|
||||
UIImage(systemName: "pin")
|
||||
),
|
||||
themeTintColor: .textPrimary,
|
||||
themeBackgroundColor: .conversationButton_swipeTertiary,
|
||||
side: .trailing,
|
||||
actionIndex: (shouldHaveBlockAction ? 0 : 1),
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { _, _, completionHandler in
|
||||
(tableView.cellForRow(at: indexPath) as? FullConversationCell)?.optimisticUpdate(
|
||||
isPinned: !(threadViewModel.threadPinnedPriority > 0)
|
||||
)
|
||||
completionHandler(true)
|
||||
|
||||
// Delay the change to give the cell "unswipe" animation some time to complete
|
||||
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) {
|
||||
Storage.shared.writeAsync { db in
|
||||
try SessionThread
|
||||
.filter(id: threadViewModel.threadId)
|
||||
.updateAllAndConfig(
|
||||
db,
|
||||
SessionThread.Columns.pinnedPriority
|
||||
.set(to: (threadViewModel.threadPinnedPriority == 0 ? 1 : 0))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard shouldHaveBlockAction else {
|
||||
return UISwipeActionsConfiguration(actions: [ delete, pin ])
|
||||
}
|
||||
|
||||
let block: UIContextualAction = UIContextualAction(
|
||||
title: (threadViewModel.threadIsBlocked == true ?
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON".localized() :
|
||||
"BLOCK_LIST_BLOCK_BUTTON".localized()
|
||||
),
|
||||
icon: UIImage(named: "table_ic_block"),
|
||||
themeTintColor: .textPrimary,
|
||||
themeBackgroundColor: .conversationButton_swipeSecondary,
|
||||
side: .trailing,
|
||||
actionIndex: 1,
|
||||
return generateSwipeActions(
|
||||
[
|
||||
(sessionIdPrefix == .blinded ? nil : .pin),
|
||||
(!shouldHaveBlockAction ? nil : .block),
|
||||
.delete
|
||||
].compactMap { $0 },
|
||||
for: .trailing,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { _, _, completionHandler in
|
||||
(tableView.cellForRow(at: indexPath) as? FullConversationCell)?.optimisticUpdate(
|
||||
isBlocked: (threadViewModel.threadIsBlocked == false)
|
||||
)
|
||||
completionHandler(true)
|
||||
|
||||
// Delay the change to give the cell "unswipe" animation some time to complete
|
||||
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) {
|
||||
Storage.shared
|
||||
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
|
||||
try Contact
|
||||
.filter(id: threadViewModel.threadId)
|
||||
.updateAllAndConfig(
|
||||
db,
|
||||
Contact.Columns.isBlocked.set(
|
||||
to: (threadViewModel.threadIsBlocked == false ?
|
||||
true:
|
||||
false
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
.sinkUntilComplete()
|
||||
}
|
||||
}
|
||||
|
||||
return UISwipeActionsConfiguration(actions: [ delete, block, pin ])
|
||||
)
|
||||
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Swipe action generation
|
||||
|
||||
private enum SwipeAction {
|
||||
case toggleReadStatus
|
||||
case hide
|
||||
case pin
|
||||
case block
|
||||
case delete
|
||||
}
|
||||
|
||||
private func generateSwipeActions(
|
||||
_ actions: [SwipeAction],
|
||||
for side: UIContextualAction.Side,
|
||||
indexPath: IndexPath,
|
||||
tableView: UITableView
|
||||
) -> UISwipeActionsConfiguration? {
|
||||
guard !actions.isEmpty else { return nil }
|
||||
|
||||
let section: HomeViewModel.SectionModel = self.viewModel.threadData[indexPath.section]
|
||||
let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row]
|
||||
let unswipeAnimationDelay: DispatchTimeInterval = .milliseconds(500)
|
||||
|
||||
// Note: for some reason the `UISwipeActionsConfiguration` expects actions to be left-to-right
|
||||
// for leading actions, but right-to-left for trailing actions...
|
||||
let targetActions: [SwipeAction] = (side == .trailing ? actions.reversed() : actions)
|
||||
|
||||
return UISwipeActionsConfiguration(
|
||||
actions: targetActions
|
||||
.enumerated()
|
||||
.map { index, action -> UIContextualAction in
|
||||
// Even though we have to reverse the actions above, the indexes in the view hierarchy
|
||||
// are in the expected order
|
||||
let targetIndex: Int = (side == .trailing ? (targetActions.count - index) : index)
|
||||
|
||||
switch action {
|
||||
// MARK: -- toggleReadStatus
|
||||
|
||||
case .toggleReadStatus:
|
||||
let isUnread: Bool = (
|
||||
threadViewModel.threadWasMarkedUnread == true ||
|
||||
(threadViewModel.threadUnreadCount ?? 0) > 0
|
||||
)
|
||||
|
||||
return UIContextualAction(
|
||||
title: (isUnread ?
|
||||
"MARK_AS_READ".localized() :
|
||||
"MARK_AS_UNREAD".localized()
|
||||
),
|
||||
icon: (isUnread ?
|
||||
UIImage(systemName: "envelope.open") :
|
||||
UIImage(systemName: "envelope.badge")
|
||||
),
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: .conversationButton_swipeRead,
|
||||
side: side,
|
||||
actionIndex: targetIndex,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { [weak self] _, _, completionHandler in
|
||||
// Delay the change to give the cell "unswipe" animation some time to complete
|
||||
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) {
|
||||
switch isUnread {
|
||||
case true:
|
||||
self?.viewModel.markAsRead(
|
||||
threadViewModel: threadViewModel,
|
||||
target: .threadAndInteractions(
|
||||
interactionsBeforeInclusive: threadViewModel.interactionId
|
||||
)
|
||||
)
|
||||
|
||||
case false:
|
||||
self?.viewModel.markAsUnread(threadViewModel: threadViewModel)
|
||||
}
|
||||
}
|
||||
completionHandler(true)
|
||||
}
|
||||
|
||||
// MARK: -- hide
|
||||
|
||||
case .hide:
|
||||
return UIContextualAction(
|
||||
title: "TXT_HIDE_TITLE".localized(),
|
||||
icon: UIImage(systemName: "eye.slash"),
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: .conversationButton_swipeDestructive,
|
||||
side: side,
|
||||
actionIndex: targetIndex,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { _, _, completionHandler in
|
||||
Storage.shared.write { db in db[.hasHiddenMessageRequests] = true }
|
||||
completionHandler(true)
|
||||
}
|
||||
|
||||
// MARK: -- pin
|
||||
|
||||
case .pin:
|
||||
return UIContextualAction(
|
||||
title: (threadViewModel.threadPinnedPriority > 0 ?
|
||||
"UNPIN_BUTTON_TEXT".localized() :
|
||||
"PIN_BUTTON_TEXT".localized()
|
||||
),
|
||||
icon: (threadViewModel.threadPinnedPriority > 0 ?
|
||||
UIImage(systemName: "pin.slash") :
|
||||
UIImage(systemName: "pin")
|
||||
),
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: .conversationButton_swipeTertiary,
|
||||
side: side,
|
||||
actionIndex: targetIndex,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { _, _, completionHandler in
|
||||
(tableView.cellForRow(at: indexPath) as? FullConversationCell)?.optimisticUpdate(
|
||||
isPinned: !(threadViewModel.threadPinnedPriority > 0)
|
||||
)
|
||||
completionHandler(true)
|
||||
|
||||
// Delay the change to give the cell "unswipe" animation some time to complete
|
||||
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) {
|
||||
Storage.shared.writeAsync { db in
|
||||
try SessionThread
|
||||
.filter(id: threadViewModel.threadId)
|
||||
.updateAllAndConfig(
|
||||
db,
|
||||
SessionThread.Columns.pinnedPriority
|
||||
.set(to: (threadViewModel.threadPinnedPriority == 0 ? 1 : 0))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -- block
|
||||
|
||||
case .block:
|
||||
return UIContextualAction(
|
||||
title: (threadViewModel.threadIsBlocked == true ?
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON".localized() :
|
||||
"BLOCK_LIST_BLOCK_BUTTON".localized()
|
||||
),
|
||||
icon: UIImage(named: "table_ic_block"),
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: .conversationButton_swipeSecondary,
|
||||
side: .trailing,
|
||||
actionIndex: 1,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { _, _, completionHandler in
|
||||
(tableView.cellForRow(at: indexPath) as? FullConversationCell)?.optimisticUpdate(
|
||||
isBlocked: (threadViewModel.threadIsBlocked == false)
|
||||
)
|
||||
completionHandler(true)
|
||||
|
||||
// Delay the change to give the cell "unswipe" animation some time to complete
|
||||
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) {
|
||||
Storage.shared
|
||||
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
|
||||
try Contact
|
||||
.filter(id: threadViewModel.threadId)
|
||||
.updateAllAndConfig(
|
||||
db,
|
||||
Contact.Columns.isBlocked.set(
|
||||
to: (threadViewModel.threadIsBlocked == false ?
|
||||
true:
|
||||
false
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
.sinkUntilComplete()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -- delete
|
||||
|
||||
case .delete:
|
||||
return UIContextualAction(
|
||||
title: "TXT_DELETE_TITLE".localized(),
|
||||
icon: UIImage(named: "icon_bin"),
|
||||
themeTintColor: .white,
|
||||
themeBackgroundColor: .conversationButton_swipeDestructive,
|
||||
side: side,
|
||||
actionIndex: targetIndex,
|
||||
indexPath: indexPath,
|
||||
tableView: tableView
|
||||
) { [weak self] _, _, completionHandler in
|
||||
let confirmationModal: ConfirmationModal = ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
title: "CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE".localized(),
|
||||
explanation: (threadViewModel.currentUserIsClosedGroupAdmin == true ?
|
||||
"admin_group_leave_warning".localized() :
|
||||
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE".localized()
|
||||
),
|
||||
confirmTitle: "TXT_DELETE_TITLE".localized(),
|
||||
confirmStyle: .danger,
|
||||
cancelStyle: .alert_text,
|
||||
dismissOnConfirm: true,
|
||||
onConfirm: { [weak self] _ in
|
||||
self?.viewModel.deleteOrLeave(
|
||||
threadId: threadViewModel.threadId,
|
||||
threadVariant: threadViewModel.threadVariant
|
||||
)
|
||||
self?.dismiss(animated: true, completion: nil)
|
||||
|
||||
completionHandler(true)
|
||||
},
|
||||
afterClosed: { completionHandler(false) }
|
||||
)
|
||||
)
|
||||
|
||||
self?.present(confirmationModal, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
||||
func handleContinueButtonTapped(from seedReminderView: SeedReminderView) {
|
||||
|
|
|
@ -173,8 +173,35 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle
|
|||
fileprivate func startNewDMIfPossible(with onsNameOrPublicKey: String) {
|
||||
let maybeSessionId: SessionId? = SessionId(from: onsNameOrPublicKey)
|
||||
|
||||
if KeyPair.isValidHexEncodedPublicKey(candidate: onsNameOrPublicKey) && maybeSessionId?.prefix == .standard {
|
||||
startNewDM(with: onsNameOrPublicKey)
|
||||
if KeyPair.isValidHexEncodedPublicKey(candidate: onsNameOrPublicKey) {
|
||||
switch maybeSessionId?.prefix {
|
||||
case .standard:
|
||||
startNewDM(with: onsNameOrPublicKey)
|
||||
|
||||
case .blinded:
|
||||
let modal: ConfirmationModal = ConfirmationModal(
|
||||
targetView: self.view,
|
||||
info: ConfirmationModal.Info(
|
||||
title: "ALERT_ERROR_TITLE".localized(),
|
||||
explanation: "DM_ERROR_DIRECT_BLINDED_ID".localized(),
|
||||
cancelTitle: "BUTTON_OK".localized(),
|
||||
cancelStyle: .alert_text
|
||||
)
|
||||
)
|
||||
self.present(modal, animated: true)
|
||||
|
||||
default:
|
||||
let modal: ConfirmationModal = ConfirmationModal(
|
||||
targetView: self.view,
|
||||
info: ConfirmationModal.Info(
|
||||
title: "ALERT_ERROR_TITLE".localized(),
|
||||
explanation: "DM_ERROR_INVALID".localized(),
|
||||
cancelTitle: "BUTTON_OK".localized(),
|
||||
cancelStyle: .alert_text
|
||||
)
|
||||
)
|
||||
self.present(modal, animated: true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -198,22 +225,12 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle
|
|||
default: break
|
||||
}
|
||||
}
|
||||
let message: String = {
|
||||
if let messageOrNil: String = messageOrNil {
|
||||
return messageOrNil
|
||||
}
|
||||
|
||||
return (maybeSessionId?.prefix == .blinded ?
|
||||
"DM_ERROR_DIRECT_BLINDED_ID".localized() :
|
||||
"DM_ERROR_INVALID".localized()
|
||||
)
|
||||
}()
|
||||
|
||||
let modal: ConfirmationModal = ConfirmationModal(
|
||||
targetView: self?.view,
|
||||
info: ConfirmationModal.Info(
|
||||
title: "ALERT_ERROR_TITLE".localized(),
|
||||
explanation: message,
|
||||
explanation: (messageOrNil ?? "DM_ERROR_INVALID".localized()),
|
||||
cancelTitle: "BUTTON_OK".localized(),
|
||||
cancelStyle: .alert_text
|
||||
)
|
||||
|
|
|
@ -530,11 +530,10 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
self.viewModel.threadVariant == .contact
|
||||
else { return }
|
||||
|
||||
let threadId: String = self.viewModel.threadId
|
||||
let threadVariant: SessionThread.Variant = self.viewModel.threadVariant
|
||||
|
||||
Storage.shared.write { db in
|
||||
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: self.viewModel.threadId) else {
|
||||
return
|
||||
}
|
||||
|
||||
try MessageSender.send(
|
||||
db,
|
||||
message: DataExtractionNotification(
|
||||
|
@ -543,7 +542,8 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
)
|
||||
),
|
||||
interactionId: nil, // Show no interaction for the current user
|
||||
in: thread
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -404,18 +404,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
}
|
||||
|
||||
self.hasInitialRootViewController = true
|
||||
self.window?.rootViewController = StyledNavigationController(
|
||||
rootViewController: {
|
||||
guard Identity.userExists() else { return LandingVC() }
|
||||
guard !Profile.fetchOrCreateCurrentUser().name.isEmpty else {
|
||||
// If we have no display name then collect one (this can happen if the
|
||||
// app crashed during onboarding which would leave the user in an invalid
|
||||
// state with no display name)
|
||||
return DisplayNameVC(flow: .register)
|
||||
}
|
||||
|
||||
return HomeVC()
|
||||
}()
|
||||
self.window?.rootViewController = TopBannerController(
|
||||
child: StyledNavigationController(
|
||||
rootViewController: {
|
||||
guard Identity.userExists() else { return LandingVC() }
|
||||
guard !Profile.fetchOrCreateCurrentUser().name.isEmpty else {
|
||||
// If we have no display name then collect one (this can happen if the
|
||||
// app crashed during onboarding which would leave the user in an invalid
|
||||
// state with no display name)
|
||||
return DisplayNameVC(flow: .register)
|
||||
}
|
||||
|
||||
return HomeVC()
|
||||
}()
|
||||
),
|
||||
cachedWarning: UserDefaults.sharedLokiProject?[.topBannerWarningToShow]
|
||||
.map { rawValue in TopBannerController.Warning(rawValue: rawValue) }
|
||||
)
|
||||
UIViewController.attemptRotationToDeviceOrientation()
|
||||
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -614,3 +614,4 @@
|
|||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
|
|
|
@ -566,7 +566,8 @@ class NotificationActionHandler {
|
|||
return try MessageSender.preparedSendData(
|
||||
db,
|
||||
interaction: interaction,
|
||||
in: thread
|
||||
threadId: thread.id,
|
||||
threadVariant: thread.variant
|
||||
)
|
||||
}
|
||||
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
|
||||
|
|
|
@ -160,8 +160,9 @@ final class RegisterVC : BaseVC {
|
|||
}
|
||||
|
||||
// MARK: Updating
|
||||
|
||||
private func updateSeed() {
|
||||
seed = Data.getSecureRandomData(ofSize: 16)!
|
||||
seed = try! Randomness.generateRandomBytes(numberBytes: 16)
|
||||
}
|
||||
|
||||
private func updateKeyPair() {
|
||||
|
|
|
@ -134,8 +134,10 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
|
|||
.preparedSendData(
|
||||
db,
|
||||
message: message,
|
||||
to: try Message.Destination.from(db, thread: thread),
|
||||
namespace: try Message.Destination.from(db, thread: thread).defaultNamespace,
|
||||
to: try Message.Destination.from(db, threadId: thread.id, threadVariant: thread.variant),
|
||||
namespace: try Message.Destination
|
||||
.from(db, threadId: thread.id, threadVariant: thread.variant)
|
||||
.defaultNamespace,
|
||||
interactionId: interactionId
|
||||
)
|
||||
)
|
||||
|
@ -194,8 +196,10 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
|
|||
sdps: [ sdp.sdp ],
|
||||
sentTimestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs())
|
||||
),
|
||||
to: try Message.Destination.from(db, thread: thread),
|
||||
namespace: try Message.Destination.from(db, thread: thread)
|
||||
to: try Message.Destination
|
||||
.from(db, threadId: thread.id, threadVariant: thread.variant),
|
||||
namespace: try Message.Destination
|
||||
.from(db, threadId: thread.id, threadVariant: thread.variant)
|
||||
.defaultNamespace,
|
||||
interactionId: nil
|
||||
)
|
||||
|
@ -259,8 +263,10 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
|
|||
kind: .answer,
|
||||
sdps: [ sdp.sdp ]
|
||||
),
|
||||
to: try Message.Destination.from(db, thread: thread),
|
||||
namespace: try Message.Destination.from(db, thread: thread)
|
||||
to: try Message.Destination
|
||||
.from(db, threadId: thread.id, threadVariant: thread.variant),
|
||||
namespace: try Message.Destination
|
||||
.from(db, threadId: thread.id, threadVariant: thread.variant)
|
||||
.defaultNamespace,
|
||||
interactionId: nil
|
||||
)
|
||||
|
@ -318,8 +324,10 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
|
|||
),
|
||||
sdps: candidates.map { $0.sdp }
|
||||
),
|
||||
to: try Message.Destination.from(db, thread: thread),
|
||||
namespace: try Message.Destination.from(db, thread: thread)
|
||||
to: try Message.Destination
|
||||
.from(db, threadId: thread.id, threadVariant: thread.variant),
|
||||
namespace: try Message.Destination
|
||||
.from(db, threadId: thread.id, threadVariant: thread.variant)
|
||||
.defaultNamespace,
|
||||
interactionId: nil
|
||||
)
|
||||
|
@ -344,8 +352,10 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
|
|||
kind: .endCall,
|
||||
sdps: []
|
||||
),
|
||||
to: try Message.Destination.from(db, thread: thread),
|
||||
namespace: try Message.Destination.from(db, thread: thread).defaultNamespace,
|
||||
to: try Message.Destination.from(db, threadId: thread.id, threadVariant: thread.variant),
|
||||
namespace: try Message.Destination
|
||||
.from(db, threadId: thread.id, threadVariant: thread.variant)
|
||||
.defaultNamespace,
|
||||
interactionId: nil
|
||||
)
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ public enum SNMessagingKit { // Just to make the external API nice
|
|||
], // Add job priorities
|
||||
[
|
||||
_011_AddPendingReadReceipts.self,
|
||||
_012_SharedUtilChanges.self,
|
||||
_012_SessionUtilChanges.self,
|
||||
// Wait until the feature is turned on before doing the migration that generates
|
||||
// the config dump data
|
||||
(Features.useSharedUtilForUserConfig ?
|
||||
|
|
|
@ -7,9 +7,9 @@ import SessionUtil
|
|||
import SessionUtilitiesKit
|
||||
|
||||
/// This migration makes the neccessary changes to support the updated user config syncing system
|
||||
enum _012_SharedUtilChanges: Migration {
|
||||
enum _012_SessionUtilChanges: Migration {
|
||||
static let target: TargetMigrations.Identifier = .messagingKit
|
||||
static let identifier: String = "SharedUtilChanges"
|
||||
static let identifier: String = "SessionUtilChanges"
|
||||
static let needsConfigSync: Bool = true
|
||||
static let minExpectedRunDuration: TimeInterval = 0.1
|
||||
|
||||
|
@ -163,10 +163,24 @@ enum _012_SharedUtilChanges: Migration {
|
|||
// If we don't have an ed25519 key then no need to create cached dump data
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
// There was previously a bug which allowed users to fully delete the 'Note to Self'
|
||||
// conversation but we don't want that, so create it again if it doesn't exists
|
||||
// Remove any hidden threads to avoid syncing them (they are basically shadow threads created
|
||||
// by starting a conversation but not sending a message so can just be cleared out)
|
||||
try SessionThread
|
||||
.fetchOrCreate(db, id: userPublicKey, variant: .contact, shouldBeVisible: false)
|
||||
.filter(
|
||||
SessionThread.Columns.shouldBeVisible == false &&
|
||||
SessionThread.Columns.id != userPublicKey
|
||||
)
|
||||
.deleteAll(db)
|
||||
|
||||
/// There was previously a bug which allowed users to fully delete the 'Note to Self' conversation but we don't want that, so
|
||||
/// create it again if it doesn't exists
|
||||
///
|
||||
/// **Note:** Since migrations are run when running tests creating a random SessionThread will result in unexpected thread
|
||||
/// counts so don't do this when running tests (this logic is the same as in `MainAppContext.isRunningTests`
|
||||
if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil {
|
||||
try SessionThread
|
||||
.fetchOrCreate(db, id: userPublicKey, variant: .contact, shouldBeVisible: false)
|
||||
}
|
||||
|
||||
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||
}
|
|
@ -55,10 +55,18 @@ enum _013_GenerateInitialUserConfigDumps: Migration {
|
|||
try SessionUtil
|
||||
.config(for: .contacts, publicKey: userPublicKey)
|
||||
.mutate { conf in
|
||||
// Exclude community, group and outgoing blinded message requests
|
||||
let validContactIds: [String] = allThreads
|
||||
.values
|
||||
.filter { thread in
|
||||
thread.variant == .contact &&
|
||||
SessionId(from: thread.id)?.prefix == .standard
|
||||
}
|
||||
.map { $0.id }
|
||||
let contactsData: [ContactInfo] = try Contact
|
||||
.filter(
|
||||
Contact.Columns.isBlocked == true ||
|
||||
allThreads.keys.contains(Contact.Columns.id)
|
||||
validContactIds.contains(Contact.Columns.id)
|
||||
)
|
||||
.including(optional: Contact.profile)
|
||||
.asRequest(of: ContactInfo.self)
|
||||
|
|
|
@ -5,6 +5,16 @@ import GRDB
|
|||
import SessionUtil
|
||||
import SessionUtilitiesKit
|
||||
|
||||
// MARK: - Size Restrictions
|
||||
|
||||
public extension SessionUtil {
|
||||
static var libSessionMaxNameByteLength: Int { CONTACT_MAX_NAME_LENGTH }
|
||||
static var libSessionMaxNicknameByteLength: Int { CONTACT_MAX_NAME_LENGTH }
|
||||
static var libSessionMaxProfileUrlByteLength: Int { PROFILE_PIC_MAX_URL_LENGTH }
|
||||
}
|
||||
|
||||
// MARK: - Contacts Handling
|
||||
|
||||
internal extension SessionUtil {
|
||||
static let columnsRelatedToContacts: [ColumnExpression] = [
|
||||
Contact.Columns.isApproved,
|
||||
|
@ -155,15 +165,14 @@ internal extension SessionUtil {
|
|||
.asRequest(of: PriorityVisibilityInfo.self)
|
||||
.fetchOne(db)
|
||||
let threadExists: Bool = (threadInfo != nil)
|
||||
let threadIsVisible: Bool = (threadInfo?.shouldBeVisible ?? false)
|
||||
|
||||
switch (data.shouldBeVisible, threadExists, threadIsVisible) {
|
||||
case (false, true, _):
|
||||
switch (data.shouldBeVisible, threadExists) {
|
||||
case (false, true):
|
||||
try SessionThread
|
||||
.filter(id: contact.id)
|
||||
.deleteAll(db)
|
||||
|
||||
case (true, false, _):
|
||||
case (true, false):
|
||||
try SessionThread(
|
||||
id: contact.id,
|
||||
variant: .contact,
|
||||
|
@ -171,9 +180,11 @@ internal extension SessionUtil {
|
|||
pinnedPriority: data.priority
|
||||
).save(db)
|
||||
|
||||
case (true, true, false):
|
||||
case (true, true):
|
||||
let changes: [ConfigColumnAssignment] = [
|
||||
SessionThread.Columns.shouldBeVisible.set(to: data.shouldBeVisible),
|
||||
(threadInfo?.shouldBeVisible == data.shouldBeVisible ? nil :
|
||||
SessionThread.Columns.shouldBeVisible.set(to: data.shouldBeVisible)
|
||||
),
|
||||
(threadInfo?.pinnedPriority == data.priority ? nil :
|
||||
SessionThread.Columns.pinnedPriority.set(to: data.priority)
|
||||
)
|
||||
|
@ -186,9 +197,33 @@ internal extension SessionUtil {
|
|||
changes
|
||||
)
|
||||
|
||||
default: break
|
||||
case (false, false): break
|
||||
}
|
||||
}
|
||||
|
||||
// Delete any contact records which have been removed
|
||||
let syncedContactIds: [String] = targetContactData
|
||||
.map { $0.key }
|
||||
.appending(userPublicKey)
|
||||
let contactIdsToRemove: [String] = try Contact
|
||||
.filter(!syncedContactIds.contains(Contact.Columns.id))
|
||||
.select(.id)
|
||||
.asRequest(of: String.self)
|
||||
.fetchAll(db)
|
||||
|
||||
if !contactIdsToRemove.isEmpty {
|
||||
try Contact
|
||||
.filter(ids: contactIdsToRemove)
|
||||
.deleteAll(db)
|
||||
|
||||
// Also need to remove any 'nickname' values since they are associated to contact data
|
||||
try Profile
|
||||
.filter(ids: contactIdsToRemove)
|
||||
.updateAll(
|
||||
db,
|
||||
Profile.Columns.nickname.set(to: nil)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Outgoing Changes
|
||||
|
@ -199,10 +234,14 @@ internal extension SessionUtil {
|
|||
) throws {
|
||||
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
||||
|
||||
// The current users contact data doesn't need to sync so exclude it
|
||||
// The current users contact data doesn't need to sync so exclude it, we also don't want to sync
|
||||
// blinded message requests so exclude those as well
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey()
|
||||
let targetContacts: [SyncedContactInfo] = contactData
|
||||
.filter { $0.id != userPublicKey }
|
||||
.filter {
|
||||
$0.id != userPublicKey &&
|
||||
SessionId(from: $0.id)?.prefix == .standard
|
||||
}
|
||||
|
||||
// If we only updated the current user contact then no need to continue
|
||||
guard !targetContacts.isEmpty else { return }
|
||||
|
@ -268,9 +307,14 @@ internal extension SessionUtil {
|
|||
static func updatingContacts<T>(_ db: Database, _ updated: [T]) throws -> [T] {
|
||||
guard let updatedContacts: [Contact] = updated as? [Contact] else { throw StorageError.generic }
|
||||
|
||||
// The current users contact data doesn't need to sync so exclude it
|
||||
// The current users contact data doesn't need to sync so exclude it, we also don't want to sync
|
||||
// blinded message requests so exclude those as well
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let targetContacts: [Contact] = updatedContacts.filter { $0.id != userPublicKey }
|
||||
let targetContacts: [Contact] = updatedContacts
|
||||
.filter {
|
||||
$0.id != userPublicKey &&
|
||||
SessionId(from: $0.id)?.prefix == .standard
|
||||
}
|
||||
|
||||
// If we only updated the current user contact then no need to continue
|
||||
guard !targetContacts.isEmpty else { return updated }
|
||||
|
@ -339,6 +383,7 @@ internal extension SessionUtil {
|
|||
let targetProfiles: [Profile] = updatedProfiles
|
||||
.filter {
|
||||
$0.id != userPublicKey &&
|
||||
SessionId(from: $0.id)?.prefix == .standard &&
|
||||
existingContactIds.contains($0.id)
|
||||
}
|
||||
|
||||
|
@ -392,8 +437,6 @@ public extension SessionUtil {
|
|||
}
|
||||
|
||||
static func remove(_ db: Database, contactIds: [String]) throws {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return }
|
||||
guard !contactIds.isEmpty else { return }
|
||||
|
||||
try SessionUtil.performAndPushChange(
|
||||
|
|
|
@ -129,7 +129,7 @@ internal extension SessionUtil {
|
|||
// update the cached config state accordingly
|
||||
guard
|
||||
let lastReadTimestampMs: Int64 = threadInfo.changes.lastReadTimestampMs,
|
||||
lastReadTimestampMs > (localThreadInfo?.changes.lastReadTimestampMs ?? 0)
|
||||
lastReadTimestampMs >= (localThreadInfo?.changes.lastReadTimestampMs ?? 0)
|
||||
else {
|
||||
// We only want to return the 'lastReadTimestampMs' change, since the local state
|
||||
// should win in that case, so ignore all others
|
||||
|
@ -299,25 +299,22 @@ public extension SessionUtil {
|
|||
threadVariant: SessionThread.Variant,
|
||||
lastReadTimestampMs: Int64
|
||||
) throws {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return }
|
||||
|
||||
let change: VolatileThreadInfo = VolatileThreadInfo(
|
||||
threadId: threadId,
|
||||
variant: threadVariant,
|
||||
openGroupUrlInfo: (threadVariant != .community ? nil :
|
||||
try OpenGroupUrlInfo.fetchOne(db, id: threadId)
|
||||
),
|
||||
changes: [.lastReadTimestampMs(lastReadTimestampMs)]
|
||||
)
|
||||
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .convoInfoVolatile,
|
||||
publicKey: getUserHexEncodedPublicKey(db)
|
||||
) { conf in
|
||||
try upsert(
|
||||
convoInfoVolatileChanges: [change],
|
||||
convoInfoVolatileChanges: [
|
||||
VolatileThreadInfo(
|
||||
threadId: threadId,
|
||||
variant: threadVariant,
|
||||
openGroupUrlInfo: (threadVariant != .community ? nil :
|
||||
try OpenGroupUrlInfo.fetchOne(db, id: threadId)
|
||||
),
|
||||
changes: [.lastReadTimestampMs(lastReadTimestampMs)]
|
||||
)
|
||||
],
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
|
|
|
@ -33,6 +33,9 @@ internal extension SessionUtil {
|
|||
publicKey: String,
|
||||
change: (UnsafeMutablePointer<config_object>?) throws -> ()
|
||||
) throws {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return }
|
||||
|
||||
// Since we are doing direct memory manipulation we are using an `Atomic`
|
||||
// type which has blocking access in it's `mutate` closure
|
||||
let needsPush: Bool
|
||||
|
@ -172,7 +175,6 @@ internal extension SessionUtil {
|
|||
.map { thread in
|
||||
LegacyGroupInfo(
|
||||
id: thread.id,
|
||||
hidden: !thread.shouldBeVisible,
|
||||
priority: thread.pinnedPriority
|
||||
.map { Int32($0 == 0 ? 0 : max($0, 1)) }
|
||||
.defaulting(to: 0)
|
||||
|
|
|
@ -6,7 +6,18 @@ import Sodium
|
|||
import SessionUtil
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
// TODO: Expose 'GROUP_NAME_MAX_LENGTH', 'COMMUNITY_URL_MAX_LENGTH' & 'COMMUNITY_ROOM_MAX_LENGTH'
|
||||
|
||||
// MARK: - Size Restrictions
|
||||
|
||||
public extension SessionUtil {
|
||||
static var libSessionMaxGroupNameByteLength: Int { GROUP_NAME_MAX_LENGTH }
|
||||
static var libSessionMaxGroupBaseUrlByteLength: Int { COMMUNITY_BASE_URL_MAX_LENGTH }
|
||||
static var libSessionMaxGroupFullUrlByteLength: Int { COMMUNITY_FULL_URL_MAX_LENGTH }
|
||||
static var libSessionMaxCommunityRoomByteLength: Int { COMMUNITY_ROOM_MAX_LENGTH }
|
||||
}
|
||||
|
||||
// MARK: - UserGroups Handling
|
||||
|
||||
internal extension SessionUtil {
|
||||
static let columnsRelatedToUserGroups: [ColumnExpression] = [
|
||||
ClosedGroup.Columns.name
|
||||
|
@ -51,18 +62,7 @@ internal extension SessionUtil {
|
|||
}
|
||||
else if user_groups_it_is_legacy_group(groupsIterator, &legacyGroup) {
|
||||
let groupId: String = String(libSessionVal: legacyGroup.session_id)
|
||||
let membersIt: OpaquePointer = ugroups_legacy_members_begin(&legacyGroup)
|
||||
var members: [String: Bool] = [:]
|
||||
var maybeMemberSessionId: UnsafePointer<CChar>? = nil
|
||||
var memberAdmin: Bool = false
|
||||
|
||||
while ugroups_legacy_members_next(membersIt, &maybeMemberSessionId, &memberAdmin) {
|
||||
guard let memberSessionId: UnsafePointer<CChar> = maybeMemberSessionId else {
|
||||
continue
|
||||
}
|
||||
|
||||
members[String(cString: memberSessionId)] = memberAdmin
|
||||
}
|
||||
let members: [String: Bool] = SessionUtil.memberInfo(in: &legacyGroup)
|
||||
|
||||
legacyGroups.append(
|
||||
LegacyGroupInfo(
|
||||
|
@ -106,7 +106,6 @@ internal extension SessionUtil {
|
|||
isHidden: false
|
||||
)
|
||||
},
|
||||
hidden: legacyGroup.hidden,
|
||||
priority: legacyGroup.priority
|
||||
)
|
||||
)
|
||||
|
@ -306,21 +305,12 @@ internal extension SessionUtil {
|
|||
}
|
||||
|
||||
// Make any thread-specific changes if needed
|
||||
let threadChanges: [ConfigColumnAssignment] = [
|
||||
(existingThreadInfo[group.id]?.shouldBeVisible == (group.hidden == false) ? nil :
|
||||
SessionThread.Columns.shouldBeVisible.set(to: (group.hidden == false))
|
||||
),
|
||||
(existingThreadInfo[group.id]?.pinnedPriority == group.priority ? nil :
|
||||
SessionThread.Columns.pinnedPriority.set(to: group.priority)
|
||||
)
|
||||
].compactMap { $0 }
|
||||
|
||||
if !threadChanges.isEmpty {
|
||||
if existingThreadInfo[group.id]?.pinnedPriority != group.priority {
|
||||
_ = try? SessionThread
|
||||
.filter(id: group.id)
|
||||
.updateAll( // Handling a config update so don't use `updateAllAndConfig`
|
||||
db,
|
||||
threadChanges
|
||||
SessionThread.Columns.pinnedPriority.set(to: group.priority)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -342,6 +332,23 @@ internal extension SessionUtil {
|
|||
// TODO: Add this
|
||||
}
|
||||
|
||||
fileprivate static func memberInfo(in legacyGroup: UnsafeMutablePointer<ugroups_legacy_group_info>) -> [String: Bool] {
|
||||
let membersIt: OpaquePointer = ugroups_legacy_members_begin(legacyGroup)
|
||||
var members: [String: Bool] = [:]
|
||||
var maybeMemberSessionId: UnsafePointer<CChar>? = nil
|
||||
var memberAdmin: Bool = false
|
||||
|
||||
while ugroups_legacy_members_next(membersIt, &maybeMemberSessionId, &memberAdmin) {
|
||||
guard let memberSessionId: UnsafePointer<CChar> = maybeMemberSessionId else {
|
||||
continue
|
||||
}
|
||||
|
||||
members[String(cString: memberSessionId)] = memberAdmin
|
||||
}
|
||||
|
||||
return members
|
||||
}
|
||||
|
||||
// MARK: - Outgoing Changes
|
||||
|
||||
static func upsert(
|
||||
|
@ -384,19 +391,54 @@ internal extension SessionUtil {
|
|||
user_groups_set_legacy_group(conf, userGroup)
|
||||
}
|
||||
|
||||
// Add the group members and admins
|
||||
legacyGroup.groupMembers?.forEach { member in
|
||||
var cProfileId: [CChar] = member.profileId.cArray
|
||||
ugroups_legacy_member_add(userGroup, &cProfileId, false)
|
||||
// Add/Remove the group members and admins
|
||||
let existingMembers: [String: Bool] = {
|
||||
guard legacyGroup.groupMembers != nil || legacyGroup.groupAdmins != nil else { return [:] }
|
||||
|
||||
return SessionUtil.memberInfo(in: userGroup)
|
||||
}()
|
||||
|
||||
if let groupMembers: [GroupMember] = legacyGroup.groupMembers {
|
||||
let memberIds: Set<String> = groupMembers.map { $0.profileId }.asSet()
|
||||
let existingMemberIds: Set<String> = Array(existingMembers
|
||||
.filter { _, isAdmin in !isAdmin }
|
||||
.keys)
|
||||
.asSet()
|
||||
let membersIdsToAdd: Set<String> = memberIds.subtracting(existingMemberIds)
|
||||
let membersIdsToRemove: Set<String> = existingMemberIds.subtracting(memberIds)
|
||||
|
||||
membersIdsToAdd.forEach { memberId in
|
||||
var cProfileId: [CChar] = memberId.cArray
|
||||
ugroups_legacy_member_add(userGroup, &cProfileId, false)
|
||||
}
|
||||
|
||||
membersIdsToRemove.forEach { memberId in
|
||||
var cProfileId: [CChar] = memberId.cArray
|
||||
ugroups_legacy_member_remove(userGroup, &cProfileId)
|
||||
}
|
||||
}
|
||||
|
||||
legacyGroup.groupAdmins?.forEach { member in
|
||||
var cProfileId: [CChar] = member.profileId.cArray
|
||||
ugroups_legacy_member_add(userGroup, &cProfileId, true)
|
||||
if let groupAdmins: [GroupMember] = legacyGroup.groupAdmins {
|
||||
let adminIds: Set<String> = groupAdmins.map { $0.profileId }.asSet()
|
||||
let existingAdminIds: Set<String> = Array(existingMembers
|
||||
.filter { _, isAdmin in isAdmin }
|
||||
.keys)
|
||||
.asSet()
|
||||
let adminIdsToAdd: Set<String> = adminIds.subtracting(existingAdminIds)
|
||||
let adminIdsToRemove: Set<String> = existingAdminIds.subtracting(adminIds)
|
||||
|
||||
adminIdsToAdd.forEach { adminId in
|
||||
var cProfileId: [CChar] = adminId.cArray
|
||||
ugroups_legacy_member_add(userGroup, &cProfileId, true)
|
||||
}
|
||||
|
||||
adminIdsToRemove.forEach { adminId in
|
||||
var cProfileId: [CChar] = adminId.cArray
|
||||
ugroups_legacy_member_remove(userGroup, &cProfileId)
|
||||
}
|
||||
}
|
||||
|
||||
// Store the updated group (can't be sure if we made any changes above)
|
||||
userGroup.pointee.hidden = (legacyGroup.hidden ?? userGroup.pointee.hidden)
|
||||
userGroup.pointee.priority = (legacyGroup.priority ?? userGroup.pointee.priority)
|
||||
|
||||
// Note: Need to free the legacy group pointer
|
||||
|
@ -441,9 +483,6 @@ public extension SessionUtil {
|
|||
rootToken: String,
|
||||
publicKey: String
|
||||
) throws {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return }
|
||||
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
|
@ -466,9 +505,6 @@ public extension SessionUtil {
|
|||
}
|
||||
|
||||
static func remove(_ db: Database, server: String, roomToken: String) throws {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return }
|
||||
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
|
@ -495,9 +531,6 @@ public extension SessionUtil {
|
|||
members: Set<String>,
|
||||
admins: Set<String>
|
||||
) throws {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return }
|
||||
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
|
@ -549,9 +582,6 @@ public extension SessionUtil {
|
|||
members: Set<String>? = nil,
|
||||
admins: Set<String>? = nil
|
||||
) throws {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return }
|
||||
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
|
@ -589,29 +619,7 @@ public extension SessionUtil {
|
|||
}
|
||||
}
|
||||
|
||||
static func hide(_ db: Database, legacyGroupIds: [String]) throws {
|
||||
guard !legacyGroupIds.isEmpty else { return }
|
||||
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
publicKey: getUserHexEncodedPublicKey(db)
|
||||
) { conf in
|
||||
try SessionUtil.upsert(
|
||||
legacyGroups: legacyGroupIds.map { groupId in
|
||||
LegacyGroupInfo(
|
||||
id: groupId,
|
||||
hidden: true
|
||||
)
|
||||
},
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static func remove(_ db: Database, legacyGroupIds: [String]) throws {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return }
|
||||
guard !legacyGroupIds.isEmpty else { return }
|
||||
|
||||
try SessionUtil.performAndPushChange(
|
||||
|
@ -630,13 +638,7 @@ public extension SessionUtil {
|
|||
|
||||
// MARK: -- Group Changes
|
||||
|
||||
static func hide(_ db: Database, groupIds: [String]) throws {
|
||||
guard !groupIds.isEmpty else { return }
|
||||
}
|
||||
|
||||
static func remove(_ db: Database, groupIds: [String]) throws {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return }
|
||||
guard !groupIds.isEmpty else { return }
|
||||
}
|
||||
}
|
||||
|
@ -653,7 +655,6 @@ extension SessionUtil {
|
|||
case disappearingConfig
|
||||
case groupMembers
|
||||
case groupAdmins
|
||||
case hidden
|
||||
case priority
|
||||
}
|
||||
|
||||
|
@ -665,7 +666,6 @@ extension SessionUtil {
|
|||
let disappearingConfig: DisappearingMessagesConfiguration?
|
||||
let groupMembers: [GroupMember]?
|
||||
let groupAdmins: [GroupMember]?
|
||||
let hidden: Bool?
|
||||
let priority: Int32?
|
||||
|
||||
init(
|
||||
|
@ -675,7 +675,6 @@ extension SessionUtil {
|
|||
disappearingConfig: DisappearingMessagesConfiguration? = nil,
|
||||
groupMembers: [GroupMember]? = nil,
|
||||
groupAdmins: [GroupMember]? = nil,
|
||||
hidden: Bool? = nil,
|
||||
priority: Int32? = nil
|
||||
) {
|
||||
self.threadId = id
|
||||
|
@ -684,7 +683,6 @@ extension SessionUtil {
|
|||
self.disappearingConfig = disappearingConfig
|
||||
self.groupMembers = groupMembers
|
||||
self.groupAdmins = groupAdmins
|
||||
self.hidden = hidden
|
||||
self.priority = priority
|
||||
}
|
||||
|
||||
|
|
|
@ -233,7 +233,7 @@ public enum SessionUtil {
|
|||
bytes: cPushData.pointee.config,
|
||||
count: cPushData.pointee.config_len
|
||||
)
|
||||
let hashesToRemove: [String] = [String](
|
||||
let obsoleteHashes: [String] = [String](
|
||||
pointer: cPushData.pointee.obsolete,
|
||||
count: cPushData.pointee.obsolete_len,
|
||||
defaultValue: []
|
||||
|
@ -248,7 +248,7 @@ public enum SessionUtil {
|
|||
data: pushData
|
||||
),
|
||||
namespace: variant.namespace,
|
||||
obsoleteHashes: hashesToRemove
|
||||
obsoleteHashes: obsoleteHashes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,30 +28,31 @@ public extension Message {
|
|||
|
||||
public static func from(
|
||||
_ db: Database,
|
||||
thread: SessionThread,
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
fileIds: [String]? = nil
|
||||
) throws -> Message.Destination {
|
||||
switch thread.variant {
|
||||
switch threadVariant {
|
||||
case .contact:
|
||||
if SessionId.Prefix(from: thread.id) == .blinded {
|
||||
guard let lookup: BlindedIdLookup = try? BlindedIdLookup.fetchOne(db, id: thread.id) else {
|
||||
if SessionId.Prefix(from: threadId) == .blinded {
|
||||
guard let lookup: BlindedIdLookup = try? BlindedIdLookup.fetchOne(db, id: threadId) else {
|
||||
preconditionFailure("Attempting to send message to blinded id without the Open Group information")
|
||||
}
|
||||
|
||||
return .openGroupInbox(
|
||||
server: lookup.openGroupServer,
|
||||
openGroupPublicKey: lookup.openGroupPublicKey,
|
||||
blindedPublicKey: thread.id
|
||||
blindedPublicKey: threadId
|
||||
)
|
||||
}
|
||||
|
||||
return .contact(publicKey: thread.id)
|
||||
return .contact(publicKey: threadId)
|
||||
|
||||
case .legacyGroup, .group:
|
||||
return .closedGroup(groupPublicKey: thread.id)
|
||||
return .closedGroup(groupPublicKey: threadId)
|
||||
|
||||
case .community:
|
||||
guard let openGroup: OpenGroup = try thread.openGroup.fetchOne(db) else {
|
||||
guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: threadId) else {
|
||||
throw StorageError.objectNotFound
|
||||
}
|
||||
|
||||
|
|
|
@ -205,7 +205,16 @@ public extension Message {
|
|||
|
||||
// Ensure we actually want to de-dupe messages for this namespace, otherwise just
|
||||
// succeed early
|
||||
guard rawMessage.namespace.shouldDedupeMessages else { return processedMessage }
|
||||
guard rawMessage.namespace.shouldDedupeMessages else {
|
||||
// If we want to track the last hash then upsert the raw message info (don't
|
||||
// want to fail if it already exsits because we don't want to dedupe messages
|
||||
// in this namespace)
|
||||
if rawMessage.namespace.shouldFetchSinceLastHash {
|
||||
_ = try rawMessage.info.saved(db)
|
||||
}
|
||||
|
||||
return processedMessage
|
||||
}
|
||||
|
||||
// Retrieve the number of entries we have for the hash of this message
|
||||
let numExistingHashes: Int = (try? SnodeReceivedMessageInfo
|
||||
|
|
|
@ -220,8 +220,10 @@ extension MessageReceiver {
|
|||
sdps: [],
|
||||
sentTimestampMs: nil // Explicitly nil as it's a separate message from above
|
||||
),
|
||||
to: try Message.Destination.from(db, thread: thread),
|
||||
namespace: try Message.Destination.from(db, thread: thread).defaultNamespace,
|
||||
to: try Message.Destination.from(db, threadId: thread.id, threadVariant: thread.variant),
|
||||
namespace: try Message.Destination
|
||||
.from(db, threadId: thread.id, threadVariant: thread.variant)
|
||||
.defaultNamespace,
|
||||
interactionId: nil // Explicitly nil as it's a separate message from above
|
||||
)
|
||||
)
|
||||
|
|
|
@ -517,24 +517,22 @@ extension MessageReceiver {
|
|||
member.role == .admin && member.profileId == sender
|
||||
})
|
||||
let members: [GroupMember] = allMembers.filter { $0.role == .standard }
|
||||
let membersToRemove: [GroupMember] = members
|
||||
let memberIdsToRemove: [String] = members
|
||||
.filter { member in
|
||||
didAdminLeave || // If the admin leaves the group is disbanded
|
||||
member.profileId == sender
|
||||
}
|
||||
let memberIdsToRemove: [String] = members.map { $0.profileId }
|
||||
.map { $0.profileId }
|
||||
|
||||
// Update libSession
|
||||
try? SessionUtil.update(
|
||||
db,
|
||||
groupPublicKey: threadId,
|
||||
members: allMembers
|
||||
.filter {
|
||||
($0.role == .standard || $0.role == .zombie) &&
|
||||
!membersToRemove.contains($0)
|
||||
}
|
||||
.filter { $0.role == .standard || $0.role == .zombie }
|
||||
.map { $0.profileId }
|
||||
.asSet(),
|
||||
.asSet()
|
||||
.subtracting(memberIdsToRemove),
|
||||
admins: allMembers
|
||||
.filter { $0.role == .admin }
|
||||
.map { $0.profileId }
|
||||
|
|
|
@ -3,12 +3,13 @@
|
|||
import Foundation
|
||||
import GRDB
|
||||
import Sodium
|
||||
import SessionUIKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
extension MessageReceiver {
|
||||
internal static func handleConfigurationMessage(_ db: Database, message: ConfigurationMessage) throws {
|
||||
internal static func handleLegacyConfigurationMessage(_ db: Database, message: ConfigurationMessage) throws {
|
||||
guard !Features.useSharedUtilForUserConfig else {
|
||||
// TODO: Show warning prompt for X days
|
||||
TopBannerController.show(warning: .outdatedUserConfig)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -112,9 +112,6 @@ extension MessageReceiver {
|
|||
.filter(ids: blindedContactIds)
|
||||
.deleteAll(db)
|
||||
|
||||
try? SessionUtil
|
||||
.remove(db, contactIds: blindedContactIds)
|
||||
|
||||
try updateContactApprovalStatusIfNeeded(
|
||||
db,
|
||||
senderSessionId: userPublicKey,
|
||||
|
|
|
@ -152,8 +152,7 @@ extension MessageSender {
|
|||
targetMembers: Set<String>,
|
||||
userPublicKey: String,
|
||||
allGroupMembers: [GroupMember],
|
||||
closedGroup: ClosedGroup,
|
||||
thread: SessionThread
|
||||
closedGroup: ClosedGroup
|
||||
) -> AnyPublisher<Void, Error> {
|
||||
guard allGroupMembers.contains(where: { $0.role == .admin && $0.profileId == userPublicKey }) else {
|
||||
return Fail(error: MessageSenderError.invalidClosedGroupUpdate)
|
||||
|
@ -203,8 +202,11 @@ extension MessageSender {
|
|||
}
|
||||
)
|
||||
),
|
||||
to: try Message.Destination.from(db, thread: thread),
|
||||
namespace: try Message.Destination.from(db, thread: thread).defaultNamespace,
|
||||
to: try Message.Destination
|
||||
.from(db, threadId: closedGroup.threadId, threadVariant: .legacyGroup),
|
||||
namespace: try Message.Destination
|
||||
.from(db, threadId: closedGroup.threadId, threadVariant: .legacyGroup)
|
||||
.defaultNamespace,
|
||||
interactionId: nil
|
||||
)
|
||||
}
|
||||
|
@ -257,12 +259,12 @@ extension MessageSender {
|
|||
name: String
|
||||
) -> AnyPublisher<Void, Error> {
|
||||
// Get the group, check preconditions & prepare
|
||||
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: groupPublicKey) else {
|
||||
guard (try? SessionThread.exists(db, id: groupPublicKey)) == true else {
|
||||
SNLog("Can't update nonexistent closed group.")
|
||||
return Fail(error: MessageSenderError.noThread)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
guard let closedGroup: ClosedGroup = try? thread.closedGroup.fetchOne(db) else {
|
||||
guard let closedGroup: ClosedGroup = try? ClosedGroup.fetchOne(db, id: groupPublicKey) else {
|
||||
return Fail(error: MessageSenderError.invalidClosedGroupUpdate)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
@ -279,7 +281,7 @@ extension MessageSender {
|
|||
|
||||
// Notify the user
|
||||
let interaction: Interaction = try Interaction(
|
||||
threadId: thread.id,
|
||||
threadId: groupPublicKey,
|
||||
authorId: userPublicKey,
|
||||
variant: .infoClosedGroupUpdated,
|
||||
body: ClosedGroupControlMessage.Kind
|
||||
|
@ -295,7 +297,8 @@ extension MessageSender {
|
|||
db,
|
||||
message: ClosedGroupControlMessage(kind: .nameChange(name: name)),
|
||||
interactionId: interactionId,
|
||||
in: thread
|
||||
threadId: groupPublicKey,
|
||||
threadVariant: .legacyGroup
|
||||
)
|
||||
|
||||
// Update libSession
|
||||
|
@ -330,8 +333,7 @@ extension MessageSender {
|
|||
addedMembers: addedMembers,
|
||||
userPublicKey: userPublicKey,
|
||||
allGroupMembers: allGroupMembers,
|
||||
closedGroup: closedGroup,
|
||||
thread: thread
|
||||
closedGroup: closedGroup
|
||||
)
|
||||
}
|
||||
catch {
|
||||
|
@ -350,8 +352,7 @@ extension MessageSender {
|
|||
removedMembers: removedMembers,
|
||||
userPublicKey: userPublicKey,
|
||||
allGroupMembers: allGroupMembers,
|
||||
closedGroup: closedGroup,
|
||||
thread: thread
|
||||
closedGroup: closedGroup
|
||||
)
|
||||
}
|
||||
catch {
|
||||
|
@ -373,10 +374,9 @@ extension MessageSender {
|
|||
addedMembers: Set<String>,
|
||||
userPublicKey: String,
|
||||
allGroupMembers: [GroupMember],
|
||||
closedGroup: ClosedGroup,
|
||||
thread: SessionThread
|
||||
closedGroup: ClosedGroup
|
||||
) throws {
|
||||
guard let disappearingMessagesConfig: DisappearingMessagesConfiguration = try thread.disappearingMessagesConfiguration.fetchOne(db) else {
|
||||
guard let disappearingMessagesConfig: DisappearingMessagesConfiguration = try DisappearingMessagesConfiguration.fetchOne(db, id: closedGroup.threadId) else {
|
||||
throw StorageError.objectNotFound
|
||||
}
|
||||
guard let encryptionKeyPair: ClosedGroupKeyPair = try closedGroup.fetchLatestKeyPair(db) else {
|
||||
|
@ -395,7 +395,7 @@ extension MessageSender {
|
|||
|
||||
// Notify the user
|
||||
let interaction: Interaction = try Interaction(
|
||||
threadId: thread.id,
|
||||
threadId: closedGroup.threadId,
|
||||
authorId: userPublicKey,
|
||||
variant: .infoClosedGroupUpdated,
|
||||
body: ClosedGroupControlMessage.Kind
|
||||
|
@ -413,7 +413,8 @@ extension MessageSender {
|
|||
members: allGroupMembers
|
||||
.filter { $0.role == .standard || $0.role == .zombie }
|
||||
.map { $0.profileId }
|
||||
.asSet(),
|
||||
.asSet()
|
||||
.union(addedMembers),
|
||||
admins: allGroupMembers
|
||||
.filter { $0.role == .admin }
|
||||
.map { $0.profileId }
|
||||
|
@ -427,13 +428,13 @@ extension MessageSender {
|
|||
kind: .membersAdded(members: addedMembers.map { Data(hex: $0) })
|
||||
),
|
||||
interactionId: interactionId,
|
||||
in: thread
|
||||
threadId: closedGroup.threadId,
|
||||
threadVariant: .legacyGroup
|
||||
)
|
||||
|
||||
try addedMembers.forEach { member in
|
||||
// Send updates to the new members individually
|
||||
let thread: SessionThread = try SessionThread
|
||||
.fetchOrCreate(db, id: member, variant: .contact, shouldBeVisible: nil)
|
||||
try SessionThread.fetchOrCreate(db, id: member, variant: .contact, shouldBeVisible: nil)
|
||||
|
||||
try MessageSender.send(
|
||||
db,
|
||||
|
@ -454,7 +455,8 @@ extension MessageSender {
|
|||
)
|
||||
),
|
||||
interactionId: nil,
|
||||
in: thread
|
||||
threadId: closedGroup.threadId,
|
||||
threadVariant: .legacyGroup
|
||||
)
|
||||
|
||||
// Add the users to the group
|
||||
|
@ -478,8 +480,7 @@ extension MessageSender {
|
|||
removedMembers: Set<String>,
|
||||
userPublicKey: String,
|
||||
allGroupMembers: [GroupMember],
|
||||
closedGroup: ClosedGroup,
|
||||
thread: SessionThread
|
||||
closedGroup: ClosedGroup
|
||||
) throws -> AnyPublisher<Void, Error> {
|
||||
guard !removedMembers.contains(userPublicKey) else {
|
||||
SNLog("Invalid closed group update.")
|
||||
|
@ -500,7 +501,7 @@ extension MessageSender {
|
|||
|
||||
// Update zombie & member list
|
||||
try GroupMember
|
||||
.filter(GroupMember.Columns.groupId == thread.id)
|
||||
.filter(GroupMember.Columns.groupId == closedGroup.threadId)
|
||||
.filter(removedMembers.contains(GroupMember.Columns.profileId))
|
||||
.filter([ GroupMember.Role.standard, GroupMember.Role.zombie ].contains(GroupMember.Columns.role))
|
||||
.deleteAll(db)
|
||||
|
@ -510,7 +511,7 @@ extension MessageSender {
|
|||
// Notify the user if needed (not if only zombie members were removed)
|
||||
if !removedMembers.subtracting(groupZombieIds).isEmpty {
|
||||
let interaction: Interaction = try Interaction(
|
||||
threadId: thread.id,
|
||||
threadId: closedGroup.threadId,
|
||||
authorId: userPublicKey,
|
||||
variant: .infoClosedGroupUpdated,
|
||||
body: ClosedGroupControlMessage.Kind
|
||||
|
@ -538,8 +539,11 @@ extension MessageSender {
|
|||
members: removedMembers.map { Data(hex: $0) }
|
||||
)
|
||||
),
|
||||
to: try Message.Destination.from(db, thread: thread),
|
||||
namespace: try Message.Destination.from(db, thread: thread).defaultNamespace,
|
||||
to: try Message.Destination
|
||||
.from(db, threadId: closedGroup.threadId, threadVariant: .legacyGroup),
|
||||
namespace: try Message.Destination
|
||||
.from(db, threadId: closedGroup.threadId, threadVariant: .legacyGroup)
|
||||
.defaultNamespace,
|
||||
interactionId: interactionId
|
||||
)
|
||||
)
|
||||
|
@ -549,8 +553,7 @@ extension MessageSender {
|
|||
targetMembers: members,
|
||||
userPublicKey: userPublicKey,
|
||||
allGroupMembers: allGroupMembers,
|
||||
closedGroup: closedGroup,
|
||||
thread: thread
|
||||
closedGroup: closedGroup
|
||||
)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
@ -605,8 +608,10 @@ extension MessageSender {
|
|||
message: ClosedGroupControlMessage(
|
||||
kind: .memberLeft
|
||||
),
|
||||
to: try Message.Destination.from(db, thread: thread),
|
||||
namespace: try Message.Destination.from(db, thread: thread).defaultNamespace,
|
||||
to: try Message.Destination.from(db, threadId: groupPublicKey, threadVariant: .legacyGroup),
|
||||
namespace: try Message.Destination
|
||||
.from(db, threadId: groupPublicKey, threadVariant: .legacyGroup)
|
||||
.defaultNamespace,
|
||||
interactionId: interactionId
|
||||
)
|
||||
|
||||
|
@ -631,6 +636,9 @@ extension MessageSender {
|
|||
}
|
||||
catch {
|
||||
switch error {
|
||||
// There are some cases where the keys for a ClosedGroup can be lost or become invalid, in
|
||||
// those cases we don't want to prevent the user from being able to leave a group so catch
|
||||
// them and just remove the group from the users devices
|
||||
case MessageSenderError.noKeyPair, MessageSenderError.encryptionFailed:
|
||||
try? ClosedGroup.removeKeysAndUnsubscribe(
|
||||
db,
|
||||
|
@ -638,6 +646,9 @@ extension MessageSender {
|
|||
removeGroupData: false,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
return Just(())
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
default: break
|
||||
}
|
||||
|
@ -721,7 +732,8 @@ extension MessageSender {
|
|||
)
|
||||
),
|
||||
interactionId: nil,
|
||||
in: thread
|
||||
threadId: thread.id,
|
||||
threadVariant: thread.variant
|
||||
)
|
||||
}
|
||||
catch {}
|
||||
|
|
|
@ -226,7 +226,7 @@ public enum MessageReceiver {
|
|||
)
|
||||
|
||||
case let message as ConfigurationMessage:
|
||||
try MessageReceiver.handleConfigurationMessage(db, message: message)
|
||||
try MessageReceiver.handleLegacyConfigurationMessage(db, message: message)
|
||||
|
||||
case let message as UnsendRequest:
|
||||
try MessageReceiver.handleUnsendRequest(
|
||||
|
|
|
@ -9,7 +9,13 @@ extension MessageSender {
|
|||
|
||||
// MARK: - Durable
|
||||
|
||||
public static func send(_ db: Database, interaction: Interaction, in thread: SessionThread, isSyncMessage: Bool = false) throws {
|
||||
public static func send(
|
||||
_ db: Database,
|
||||
interaction: Interaction,
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
isSyncMessage: Bool = false
|
||||
) throws {
|
||||
// Only 'VisibleMessage' types can be sent via this method
|
||||
guard interaction.variant == .standardOutgoing else { throw MessageSenderError.invalidMessage }
|
||||
guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved }
|
||||
|
@ -17,25 +23,39 @@ extension MessageSender {
|
|||
send(
|
||||
db,
|
||||
message: VisibleMessage.from(db, interaction: interaction),
|
||||
threadId: thread.id,
|
||||
threadId: threadId,
|
||||
interactionId: interactionId,
|
||||
to: try Message.Destination.from(db, thread: thread),
|
||||
to: try Message.Destination.from(db, threadId: threadId, threadVariant: threadVariant),
|
||||
isSyncMessage: isSyncMessage
|
||||
)
|
||||
}
|
||||
|
||||
public static func send(_ db: Database, message: Message, interactionId: Int64?, in thread: SessionThread, isSyncMessage: Bool = false) throws {
|
||||
public static func send(
|
||||
_ db: Database,
|
||||
message: Message,
|
||||
interactionId: Int64?,
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
isSyncMessage: Bool = false
|
||||
) throws {
|
||||
send(
|
||||
db,
|
||||
message: message,
|
||||
threadId: thread.id,
|
||||
threadId: threadId,
|
||||
interactionId: interactionId,
|
||||
to: try Message.Destination.from(db, thread: thread),
|
||||
to: try Message.Destination.from(db, threadId: threadId, threadVariant: threadVariant),
|
||||
isSyncMessage: isSyncMessage
|
||||
)
|
||||
}
|
||||
|
||||
public static func send(_ db: Database, message: Message, threadId: String?, interactionId: Int64?, to destination: Message.Destination, isSyncMessage: Bool = false) {
|
||||
public static func send(
|
||||
_ db: Database,
|
||||
message: Message,
|
||||
threadId: String?,
|
||||
interactionId: Int64?,
|
||||
to destination: Message.Destination,
|
||||
isSyncMessage: Bool = false
|
||||
) {
|
||||
// If it's a sync message then we need to make some slight tweaks before sending so use the proper
|
||||
// sync message sending process instead of the standard process
|
||||
guard !isSyncMessage else {
|
||||
|
@ -70,17 +90,20 @@ extension MessageSender {
|
|||
public static func preparedSendData(
|
||||
_ db: Database,
|
||||
interaction: Interaction,
|
||||
in thread: SessionThread
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant
|
||||
) throws -> PreparedSendData {
|
||||
// Only 'VisibleMessage' types can be sent via this method
|
||||
guard interaction.variant == .standardOutgoing else { throw MessageSenderError.invalidMessage }
|
||||
guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved }
|
||||
|
||||
|
||||
return try MessageSender.preparedSendData(
|
||||
db,
|
||||
message: VisibleMessage.from(db, interaction: interaction),
|
||||
to: try Message.Destination.from(db, thread: thread),
|
||||
namespace: try Message.Destination.from(db, thread: thread).defaultNamespace,
|
||||
to: try Message.Destination.from(db, threadId: threadId, threadVariant: threadVariant),
|
||||
namespace: try Message.Destination
|
||||
.from(db, threadId: threadId, threadVariant: threadVariant)
|
||||
.defaultNamespace,
|
||||
interactionId: interactionId
|
||||
)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ public class TypingIndicators {
|
|||
|
||||
private class Indicator {
|
||||
fileprivate let threadId: String
|
||||
fileprivate let threadVariant: SessionThread.Variant
|
||||
fileprivate let direction: Direction
|
||||
fileprivate let timestampMs: Int64
|
||||
|
||||
|
@ -45,6 +46,7 @@ public class TypingIndicators {
|
|||
else { return nil }
|
||||
|
||||
self.threadId = threadId
|
||||
self.threadVariant = threadVariant
|
||||
self.direction = direction
|
||||
self.timestampMs = (timestampMs ?? SnodeAPI.currentOffsetTimestampMs())
|
||||
}
|
||||
|
@ -75,15 +77,12 @@ public class TypingIndicators {
|
|||
|
||||
switch direction {
|
||||
case .outgoing:
|
||||
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: self.threadId) else {
|
||||
return
|
||||
}
|
||||
|
||||
try? MessageSender.send(
|
||||
db,
|
||||
message: TypingIndicator(kind: .stopped),
|
||||
interactionId: nil,
|
||||
in: thread
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant
|
||||
)
|
||||
|
||||
case .incoming:
|
||||
|
@ -111,15 +110,12 @@ public class TypingIndicators {
|
|||
|
||||
private func scheduleRefreshCallback(_ db: Database, shouldSend: Bool = true) {
|
||||
if shouldSend {
|
||||
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: self.threadId) else {
|
||||
return
|
||||
}
|
||||
|
||||
try? MessageSender.send(
|
||||
db,
|
||||
message: TypingIndicator(kind: .started),
|
||||
interactionId: nil,
|
||||
in: thread
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,6 @@ public struct ProfileManager {
|
|||
|
||||
// The max bytes for a user's profile name, encoded in UTF8.
|
||||
// Before encrypting and submitting we NULL pad the name data to this length.
|
||||
private static let nameDataLength: UInt = 64
|
||||
public static let maxAvatarDiameter: CGFloat = 640
|
||||
private static let maxAvatarBytes: UInt = (5 * 1000 * 1000)
|
||||
public static let avatarAES256KeyByteLength: Int = 32
|
||||
|
@ -45,7 +44,11 @@ public struct ProfileManager {
|
|||
// MARK: - Functions
|
||||
|
||||
public static func isToLong(profileName: String) -> Bool {
|
||||
return ((profileName.data(using: .utf8)?.count ?? 0) > nameDataLength)
|
||||
return (profileName.utf8CString.count > SessionUtil.libSessionMaxNameByteLength)
|
||||
}
|
||||
|
||||
public static func isToLong(profileUrl: String) -> Bool {
|
||||
return (profileUrl.utf8CString.count > SessionUtil.libSessionMaxProfileUrlByteLength)
|
||||
}
|
||||
|
||||
public static func profileAvatar(_ db: Database? = nil, id: String) -> Data? {
|
||||
|
|
|
@ -190,9 +190,13 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
|
||||
Storage.shared
|
||||
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db -> MessageSender.PreparedSendData in
|
||||
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
|
||||
throw MessageSenderError.noThread
|
||||
}
|
||||
guard
|
||||
let threadVariant: SessionThread.Variant = try SessionThread
|
||||
.filter(id: threadId)
|
||||
.select(.variant)
|
||||
.asRequest(of: SessionThread.Variant.self)
|
||||
.fetchOne(db)
|
||||
else { throw MessageSenderError.noThread }
|
||||
|
||||
// Create the interaction
|
||||
let interaction: Interaction = try Interaction(
|
||||
|
@ -245,7 +249,8 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
.preparedSendData(
|
||||
db,
|
||||
interaction: interaction,
|
||||
in: thread
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant
|
||||
)
|
||||
}
|
||||
.flatMap {
|
||||
|
|
|
@ -9,7 +9,6 @@ public final class SnodeMessage: Codable {
|
|||
case data
|
||||
case ttl
|
||||
case timestampMs = "timestamp"
|
||||
case nonce
|
||||
}
|
||||
|
||||
/// The hex encoded public key of the recipient.
|
||||
|
@ -57,6 +56,5 @@ extension SnodeMessage {
|
|||
try container.encode(data, forKey: .data)
|
||||
try container.encode(ttl, forKey: .ttl)
|
||||
try container.encode(timestampMs, forKey: .timestampMs)
|
||||
try container.encode("", forKey: .nonce)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -305,7 +305,7 @@ public final class SnodeAPI {
|
|||
.map { _ -> [SnodeAPI.Namespace: String] in
|
||||
namespaces
|
||||
.reduce(into: [:]) { result, namespace in
|
||||
guard namespace.shouldDedupeMessages else { return }
|
||||
guard namespace.shouldFetchSinceLastHash else { return }
|
||||
|
||||
// Prune expired message hashes for this namespace on this service node
|
||||
SnodeReceivedMessageInfo.pruneExpiredMessageHashInfo(
|
||||
|
@ -472,7 +472,7 @@ public final class SnodeAPI {
|
|||
}
|
||||
.tryFlatMap { lastHash -> AnyPublisher<(info: ResponseInfoType, data: GetMessagesResponse?, lastHash: String?), Error> in
|
||||
|
||||
guard namespace.requiresWriteAuthentication else {
|
||||
guard namespace.requiresReadAuthentication else {
|
||||
return SnodeAPI
|
||||
.send(
|
||||
request: SnodeRequest(
|
||||
|
|
|
@ -18,6 +18,7 @@ public extension SnodeAPI {
|
|||
|
||||
var requiresReadAuthentication: Bool {
|
||||
switch self {
|
||||
// Legacy closed groups don't support authenticated retrieval
|
||||
case .legacyClosedGroup: return false
|
||||
default: return true
|
||||
}
|
||||
|
@ -25,12 +26,17 @@ public extension SnodeAPI {
|
|||
|
||||
var requiresWriteAuthentication: Bool {
|
||||
switch self {
|
||||
// Not in use until we can batch delete and store config messages
|
||||
case .default, .legacyClosedGroup: return false
|
||||
// Legacy closed groups don't support authenticated storage
|
||||
case .legacyClosedGroup: return false
|
||||
default: return true
|
||||
}
|
||||
}
|
||||
|
||||
/// This flag indicates whether we should provide a `lastHash` when retrieving messages from the specified
|
||||
/// namespace, when `true` we will only receive messages added since the provided `lastHash`, otherwise
|
||||
/// we will retrieve **all** messages from the namespace
|
||||
public var shouldFetchSinceLastHash: Bool { true }
|
||||
|
||||
/// This flag indicates whether we should dedupe messages from the specified namespace, when `true` we will
|
||||
/// store a `SnodeReceivedMessageInfo` record for the message and check for a matching record whenever
|
||||
/// we receive a message from this namespace
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public class TopBannerController: UIViewController {
|
||||
public enum Warning: String, Codable {
|
||||
case outdatedUserConfig
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
case .outdatedUserConfig: return "USER_CONFIG_OUTDATED_WARNING".localized()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static var lastInstance: TopBannerController?
|
||||
private let child: UIViewController
|
||||
private var initialCachedWarning: Warning?
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
private lazy var bottomConstraint: NSLayoutConstraint = bannerLabel
|
||||
.pin(.bottom, to: .bottom, of: bannerContainer, withInset: -Values.verySmallSpacing)
|
||||
|
||||
private let contentStackView: UIStackView = {
|
||||
let result: UIStackView = UIStackView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.axis = .vertical
|
||||
result.distribution = .fill
|
||||
result.alignment = .fill
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let bannerContainer: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.themeBackgroundColor = .primary
|
||||
result.isHidden = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let bannerLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setContentHuggingPriority(.required, for: .vertical)
|
||||
result.font = .systemFont(ofSize: Values.verySmallFontSize)
|
||||
result.textAlignment = .center
|
||||
result.themeTextColor = .black
|
||||
result.numberOfLines = 0
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var closeButton: UIButton = {
|
||||
let result: UIButton = UIButton()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setImage(
|
||||
UIImage(systemName: "xmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .bold))?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
for: .normal
|
||||
)
|
||||
result.contentMode = .center
|
||||
result.themeTintColor = .black
|
||||
result.addTarget(self, action: #selector(dismissBanner), for: .touchUpInside)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(
|
||||
child: UIViewController,
|
||||
cachedWarning: Warning? = nil
|
||||
) {
|
||||
self.child = child
|
||||
self.initialCachedWarning = cachedWarning
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
TopBannerController.lastInstance = self
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
public override func loadView() {
|
||||
super.loadView()
|
||||
|
||||
view.addSubview(contentStackView)
|
||||
|
||||
contentStackView.addArrangedSubview(bannerContainer)
|
||||
|
||||
child.willMove(toParent: self)
|
||||
addChild(child)
|
||||
contentStackView.addArrangedSubview(child.view)
|
||||
child.didMove(toParent: self)
|
||||
|
||||
bannerContainer.addSubview(bannerLabel)
|
||||
bannerContainer.addSubview(closeButton)
|
||||
|
||||
setupLayout()
|
||||
|
||||
// If we had an initial warning then show it
|
||||
if let warning: Warning = self.initialCachedWarning {
|
||||
UIView.performWithoutAnimation {
|
||||
TopBannerController.show(warning: warning)
|
||||
}
|
||||
|
||||
self.initialCachedWarning = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func setupLayout() {
|
||||
contentStackView.pin(.top, to: .top, of: view.safeAreaLayoutGuide)
|
||||
contentStackView.pin(.leading, to: .leading, of: view)
|
||||
contentStackView.pin(.trailing, to: .trailing, of: view)
|
||||
contentStackView.pin(.bottom, to: .bottom, of: view)
|
||||
|
||||
bannerLabel.pin(.top, to: .top, of: view.safeAreaLayoutGuide, withInset: Values.verySmallSpacing)
|
||||
bannerLabel.pin(.leading, to: .leading, of: bannerContainer, withInset: Values.veryLargeSpacing)
|
||||
bannerLabel.pin(.trailing, to: .trailing, of: bannerContainer, withInset: -Values.veryLargeSpacing)
|
||||
bottomConstraint.isActive = false
|
||||
|
||||
let buttonSize: CGFloat = (12 + (Values.smallSpacing * 2))
|
||||
closeButton.center(.vertical, in: bannerLabel)
|
||||
closeButton.pin(.trailing, to: .trailing, of: bannerContainer, withInset: -Values.smallSpacing)
|
||||
closeButton.set(.width, to: buttonSize)
|
||||
closeButton.set(.height, to: buttonSize)
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc private func dismissBanner() {
|
||||
// Remove the cached warning
|
||||
UserDefaults.sharedLokiProject?[.topBannerWarningToShow] = nil
|
||||
|
||||
UIView.animate(
|
||||
withDuration: 0.3,
|
||||
animations: { [weak self] in
|
||||
self?.bottomConstraint.isActive = false
|
||||
self?.contentStackView.setNeedsLayout()
|
||||
self?.contentStackView.layoutIfNeeded()
|
||||
},
|
||||
completion: { [weak self] _ in
|
||||
self?.bannerContainer.isHidden = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
public static func show(warning: Warning, inWindowFor view: UIView? = nil) {
|
||||
guard Thread.isMainThread else {
|
||||
DispatchQueue.main.async {
|
||||
TopBannerController.show(warning: warning, inWindowFor: view)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Not an ideal approach but should allow
|
||||
guard let instance: TopBannerController = ((view?.window?.rootViewController as? TopBannerController) ?? TopBannerController.lastInstance) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Cache the banner to show (so we can show it on re-launch)
|
||||
UserDefaults.sharedLokiProject?[.topBannerWarningToShow] = warning.rawValue
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
instance.bannerLabel.text = warning.text
|
||||
instance.bannerLabel.setNeedsLayout()
|
||||
instance.bannerLabel.layoutIfNeeded()
|
||||
instance.bottomConstraint.isActive = false
|
||||
instance.bannerContainer.isHidden = false
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: 0.3) { [weak instance] in
|
||||
instance?.bottomConstraint.isActive = true
|
||||
instance?.contentStackView.setNeedsLayout()
|
||||
instance?.contentStackView.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -76,6 +76,18 @@ public extension Anchorable {
|
|||
.setting(isActive: true)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pin(_ constraineeEdge: UIView.HorizontalEdge, greaterThanOrEqualTo constrainerEdge: UIView.HorizontalEdge, of anchorable: Anchorable, withInset inset: CGFloat = 0) -> NSLayoutConstraint {
|
||||
(self as? UIView)?.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
return anchor(from: constraineeEdge)
|
||||
.constraint(
|
||||
greaterThanOrEqualTo: anchorable.anchor(from: constrainerEdge),
|
||||
constant: inset
|
||||
)
|
||||
.setting(isActive: true)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pin(_ constraineeEdge: UIView.VerticalEdge, to constrainerEdge: UIView.VerticalEdge, of anchorable: Anchorable, withInset inset: CGFloat = 0) -> NSLayoutConstraint {
|
||||
(self as? UIView)?.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -87,6 +99,18 @@ public extension Anchorable {
|
|||
)
|
||||
.setting(isActive: true)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pin(_ constraineeEdge: UIView.VerticalEdge, greaterThanOrEqualTo constrainerEdge: UIView.VerticalEdge, of anchorable: Anchorable, withInset inset: CGFloat = 0) -> NSLayoutConstraint {
|
||||
(self as? UIView)?.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
return anchor(from: constraineeEdge)
|
||||
.constraint(
|
||||
greaterThanOrEqualTo: anchorable.anchor(from: constrainerEdge),
|
||||
constant: inset
|
||||
)
|
||||
.setting(isActive: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - View extensions
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Data {
|
||||
|
||||
/// Returns `size` bytes of random data generated using the default secure random number generator. See
|
||||
/// [SecRandomCopyBytes](https://developer.apple.com/documentation/security/1399291-secrandomcopybytes) for more information.
|
||||
static func getSecureRandomData(ofSize size: UInt) -> Data? {
|
||||
var data = Data(count: Int(size))
|
||||
let result = data.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, Int(size), $0.baseAddress!) }
|
||||
guard result == errSecSuccess else { return nil }
|
||||
return data
|
||||
}
|
||||
}
|
|
@ -59,6 +59,7 @@ public enum SNUserDefaults {
|
|||
|
||||
public enum String : Swift.String {
|
||||
case deviceToken
|
||||
case topBannerWarningToShow
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,17 +3,19 @@
|
|||
import Foundation
|
||||
|
||||
public enum Randomness {
|
||||
/// Returns `size` bytes of random data generated using the default secure random number generator. See
|
||||
/// [SecRandomCopyBytes](https://developer.apple.com/documentation/security/1399291-secrandomcopybytes) for more information.
|
||||
public static func generateRandomBytes(numberBytes: Int) throws -> Data {
|
||||
var randomByes: Data = Data(count: numberBytes)
|
||||
let result = randomByes.withUnsafeMutableBytes {
|
||||
var randomBytes: Data = Data(count: numberBytes)
|
||||
let result = randomBytes.withUnsafeMutableBytes {
|
||||
SecRandomCopyBytes(kSecRandomDefault, numberBytes, $0.baseAddress!)
|
||||
}
|
||||
|
||||
guard result == errSecSuccess, randomByes.count == numberBytes else {
|
||||
guard result == errSecSuccess, randomBytes.count == numberBytes else {
|
||||
print("Problem generating random bytes")
|
||||
throw GeneralError.randomGenerationFailed
|
||||
}
|
||||
|
||||
return randomByes
|
||||
return randomBytes
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
@testable import SessionUtilitiesKit
|
||||
|
||||
class ArrayUtilitiesSpec: QuickSpec {
|
||||
private struct TestType: Equatable {
|
||||
let stringValue: String
|
||||
let intValue: Int
|
||||
}
|
||||
|
||||
// MARK: - Spec
|
||||
|
||||
override func spec() {
|
||||
describe("an Array") {
|
||||
context("when grouping") {
|
||||
it("maintains the original array ordering") {
|
||||
let data: [TestType] = [
|
||||
TestType(stringValue: "b", intValue: 5),
|
||||
TestType(stringValue: "A", intValue: 2),
|
||||
TestType(stringValue: "z", intValue: 1),
|
||||
TestType(stringValue: "x", intValue: 3),
|
||||
TestType(stringValue: "7", intValue: 6),
|
||||
TestType(stringValue: "A", intValue: 7),
|
||||
TestType(stringValue: "z", intValue: 8),
|
||||
TestType(stringValue: "7", intValue: 9),
|
||||
TestType(stringValue: "7", intValue: 4),
|
||||
TestType(stringValue: "h", intValue: 2),
|
||||
TestType(stringValue: "z", intValue: 1),
|
||||
TestType(stringValue: "m", intValue: 2)
|
||||
]
|
||||
|
||||
let result1: [String: [TestType]] = data.grouped(by: \.stringValue)
|
||||
let result2: [Int: [TestType]] = data.grouped(by: \.intValue)
|
||||
|
||||
expect(result1).to(equal(
|
||||
[
|
||||
"b": [TestType(stringValue: "b", intValue: 5)],
|
||||
"A": [
|
||||
TestType(stringValue: "A", intValue: 2),
|
||||
TestType(stringValue: "A", intValue: 7)
|
||||
],
|
||||
"z": [
|
||||
TestType(stringValue: "z", intValue: 1),
|
||||
TestType(stringValue: "z", intValue: 8),
|
||||
TestType(stringValue: "z", intValue: 1)
|
||||
],
|
||||
"x": [TestType(stringValue: "x", intValue: 3)],
|
||||
"7": [
|
||||
TestType(stringValue: "7", intValue: 6),
|
||||
TestType(stringValue: "7", intValue: 9),
|
||||
TestType(stringValue: "7", intValue: 4)
|
||||
],
|
||||
"h": [TestType(stringValue: "h", intValue: 2)],
|
||||
"m": [TestType(stringValue: "m", intValue: 2)]
|
||||
]
|
||||
))
|
||||
expect(result2).to(equal(
|
||||
[
|
||||
1: [
|
||||
TestType(stringValue: "z", intValue: 1),
|
||||
TestType(stringValue: "z", intValue: 1),
|
||||
],
|
||||
2: [
|
||||
TestType(stringValue: "A", intValue: 2),
|
||||
TestType(stringValue: "h", intValue: 2),
|
||||
TestType(stringValue: "m", intValue: 2)
|
||||
],
|
||||
3: [TestType(stringValue: "x", intValue: 3)],
|
||||
4: [TestType(stringValue: "7", intValue: 4)],
|
||||
5: [TestType(stringValue: "b", intValue: 5)],
|
||||
6: [TestType(stringValue: "7", intValue: 6)],
|
||||
7: [TestType(stringValue: "A", intValue: 7)],
|
||||
9: [TestType(stringValue: "7", intValue: 9)],
|
||||
8: [TestType(stringValue: "z", intValue: 8)]
|
||||
]
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue