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:
Morgan Pretty 2023-03-08 17:27:07 +11:00
parent 972519d7d9
commit 66fd2d4ff8
64 changed files with 1022 additions and 511 deletions

View File

@ -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 */,
);

View File

@ -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())
}

View File

@ -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 {

View File

@ -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(

View File

@ -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

View File

@ -750,7 +750,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
try MessageSender.send(
db,
interaction: interaction,
in: thread
threadId: thread.id,
threadVariant: thread.variant
)
}
}

View File

@ -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) {

View File

@ -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
)

View File

@ -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
)
}
}

View File

@ -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()

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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) }

View File

@ -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() {

View File

@ -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
)

View File

@ -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 ?

View File

@ -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
}

View File

@ -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)

View File

@ -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(

View File

@ -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
)
}

View File

@ -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)

View File

@ -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
}

View File

@ -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
)
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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
)
)

View File

@ -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 }

View File

@ -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
}

View File

@ -112,9 +112,6 @@ extension MessageReceiver {
.filter(ids: blindedContactIds)
.deleteAll(db)
try? SessionUtil
.remove(db, contactIds: blindedContactIds)
try updateContactApprovalStatusIfNeeded(
db,
senderSessionId: userPublicKey,

View File

@ -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 {}

View File

@ -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(

View File

@ -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
)
}

View File

@ -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
)
}

View File

@ -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? {

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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(

View File

@ -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

View File

@ -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()
}
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -59,6 +59,7 @@ public enum SNUserDefaults {
public enum String : Swift.String {
case deviceToken
case topBannerWarningToShow
}
}

View File

@ -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
}
}

View File

@ -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)]
]
))
}
}
}
}
}