Cleaned up a bunch of code, added pinned and hidden handling

Added in logic to handle the 'hidden' state
Replaced the 'Group Created' message with an empty state
Cleaned up a bunch of boilerplate code
This commit is contained in:
Morgan Pretty 2023-03-02 17:52:37 +11:00
parent 8eed08b5b4
commit 7ee84fe0d3
69 changed files with 1391 additions and 1043 deletions

View File

@ -457,7 +457,7 @@ extension ConversationVC:
// Update the thread to be visible // Update the thread to be visible
_ = try SessionThread _ = try SessionThread
.filter(id: threadId) .filter(id: threadId)
.updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true)) .updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
let authorId: String = { let authorId: String = {
if let blindedId = self?.viewModel.threadData.currentUserBlindedPublicKey { if let blindedId = self?.viewModel.threadData.currentUserBlindedPublicKey {
@ -588,7 +588,7 @@ extension ConversationVC:
// Update the thread to be visible // Update the thread to be visible
_ = try SessionThread _ = try SessionThread
.filter(id: threadId) .filter(id: threadId)
.updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true)) .updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
// Create the interaction // Create the interaction
let interaction: Interaction = try Interaction( let interaction: Interaction = try Interaction(
@ -1104,7 +1104,8 @@ extension ConversationVC:
guard viewModel.threadData.canWrite else { return } guard viewModel.threadData.canWrite else { return }
guard SessionId.Prefix(from: sessionId) == .blinded else { guard SessionId.Prefix(from: sessionId) == .blinded else {
Storage.shared.write { db in Storage.shared.write { db in
try SessionThread.fetchOrCreate(db, id: sessionId, variant: .contact) try SessionThread
.fetchOrCreate(db, id: sessionId, variant: .contact, shouldBeVisible: nil)
} }
let conversationVC: ConversationVC = ConversationVC(threadId: sessionId, threadVariant: .contact) let conversationVC: ConversationVC = ConversationVC(threadId: sessionId, threadVariant: .contact)
@ -1130,7 +1131,12 @@ extension ConversationVC:
) )
return try SessionThread return try SessionThread
.fetchOrCreate(db, id: (lookup.sessionId ?? lookup.blindedId), variant: .contact) .fetchOrCreate(
db,
id: (lookup.sessionId ?? lookup.blindedId),
variant: .contact,
shouldBeVisible: nil
)
.id .id
} }
@ -1308,7 +1314,7 @@ extension ConversationVC:
// Update the thread to be visible // Update the thread to be visible
_ = try SessionThread _ = try SessionThread
.filter(id: thread.id) .filter(id: thread.id)
.updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true)) .updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
let pendingReaction: Reaction? = { let pendingReaction: Reaction? = {
if remove { if remove {

View File

@ -206,6 +206,35 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
return result return result
}() }()
private lazy var emptyStateLabel: UILabel = {
let text: String = String(
format: "GROUP_CONVERSATION_EMPTY_STATE".localized(),
self.viewModel.threadData.displayName
)
let result: UILabel = UILabel()
result.accessibilityLabel = "Empty state label"
result.translatesAutoresizingMaskIntoConstraints = false
result.font = .systemFont(ofSize: Values.verySmallFontSize)
result.attributedText = NSAttributedString(string: text)
.adding(
attributes: [.font: UIFont.boldSystemFont(ofSize: Values.verySmallFontSize)],
range: text.range(of: self.viewModel.threadData.displayName)
.map { NSRange($0, in: text) }
.defaulting(to: NSRange(location: 0, length: 0))
)
result.themeTextColor = .textSecondary
result.textAlignment = .center
result.lineBreakMode = .byWordWrapping
result.numberOfLines = 0
result.isHidden = (
self.viewModel.threadData.threadVariant != .legacyGroup &&
self.viewModel.threadData.threadVariant != .group
)
return result
}()
lazy var footerControlsStackView: UIStackView = { lazy var footerControlsStackView: UIStackView = {
let result: UIStackView = UIStackView() let result: UIStackView = UIStackView()
@ -367,8 +396,13 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
// Message requests view & scroll to bottom // Message requests view & scroll to bottom
view.addSubview(scrollButton) view.addSubview(scrollButton)
view.addSubview(emptyStateLabel)
view.addSubview(messageRequestBackgroundView) view.addSubview(messageRequestBackgroundView)
view.addSubview(messageRequestStackView) view.addSubview(messageRequestStackView)
emptyStateLabel.pin(.top, to: .top, of: view, withInset: Values.largeSpacing)
emptyStateLabel.pin(.leading, to: .leading, of: view, withInset: Values.largeSpacing)
emptyStateLabel.pin(.trailing, to: .trailing, of: view, withInset: -Values.largeSpacing)
messageRequestStackView.addArrangedSubview(messageRequestBlockButton) messageRequestStackView.addArrangedSubview(messageRequestBlockButton)
messageRequestStackView.addArrangedSubview(messageRequestDescriptionContainerView) messageRequestStackView.addArrangedSubview(messageRequestDescriptionContainerView)
@ -376,7 +410,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
messageRequestDescriptionContainerView.addSubview(messageRequestDescriptionLabel) messageRequestDescriptionContainerView.addSubview(messageRequestDescriptionLabel)
messageRequestActionStackView.addArrangedSubview(messageRequestAcceptButton) messageRequestActionStackView.addArrangedSubview(messageRequestAcceptButton)
messageRequestActionStackView.addArrangedSubview(messageRequestDeleteButton) messageRequestActionStackView.addArrangedSubview(messageRequestDeleteButton)
scrollButton.pin(.trailing, to: .trailing, of: view, withInset: -20) scrollButton.pin(.trailing, to: .trailing, of: view, withInset: -20)
messageRequestStackView.pin(.leading, to: .leading, of: view, withInset: 16) messageRequestStackView.pin(.leading, to: .leading, of: view, withInset: 16)
messageRequestStackView.pin(.trailing, to: .trailing, of: view, withInset: -16) messageRequestStackView.pin(.trailing, to: .trailing, of: view, withInset: -16)
@ -618,6 +652,19 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
onlyNotifyForMentions: (updatedThreadData.threadOnlyNotifyForMentions == true), onlyNotifyForMentions: (updatedThreadData.threadOnlyNotifyForMentions == true),
userCount: updatedThreadData.userCount userCount: updatedThreadData.userCount
) )
// Update the empty state
let text: String = String(
format: "GROUP_CONVERSATION_EMPTY_STATE".localized(),
updatedThreadData.displayName
)
emptyStateLabel.attributedText = NSAttributedString(string: text)
.adding(
attributes: [.font: UIFont.boldSystemFont(ofSize: Values.verySmallFontSize)],
range: text.range(of: updatedThreadData.displayName)
.map { NSRange($0, in: text) }
.defaulting(to: NSRange(location: 0, length: 0))
)
} }
if if
@ -718,6 +765,19 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
self.hasLoadedInitialInteractionData = true self.hasLoadedInitialInteractionData = true
self.viewModel.updateInteractionData(updatedData) self.viewModel.updateInteractionData(updatedData)
// Update the empty state
let hasMessages: Bool = (updatedData
.filter { $0.model == .messages }
.first?
.elements
.isEmpty == false)
self.emptyStateLabel.isHidden = (
hasMessages || (
self.viewModel.threadData.threadVariant != .legacyGroup &&
self.viewModel.threadData.threadVariant != .group
)
)
UIView.performWithoutAnimation { UIView.performWithoutAnimation {
self.tableView.reloadData() self.tableView.reloadData()
self.performInitialScrollIfNeeded() self.performInitialScrollIfNeeded()
@ -726,6 +786,14 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
return return
} }
// Update the empty state
self.emptyStateLabel.isHidden = (
!updatedData.isEmpty || (
self.viewModel.threadData.threadVariant != .legacyGroup &&
self.viewModel.threadData.threadVariant != .group
)
)
// Update the ReactionListSheet (if one exists) // Update the ReactionListSheet (if one exists)
if let messageUpdates: [MessageViewModel] = updatedData.first(where: { $0.model == .messages })?.elements { if let messageUpdates: [MessageViewModel] = updatedData.first(where: { $0.model == .messages })?.elements {
self.currentReactionListSheet?.handleInteractionUpdates(messageUpdates) self.currentReactionListSheet?.handleInteractionUpdates(messageUpdates)

View File

@ -6,7 +6,7 @@ import SessionMessagingKit
final class InfoMessageCell: MessageCell { final class InfoMessageCell: MessageCell {
private static let iconSize: CGFloat = 16 private static let iconSize: CGFloat = 16
private static let inset = Values.mediumSpacing public static let inset = Values.mediumSpacing
private var isHandlingLongPress: Bool = false private var isHandlingLongPress: Bool = false

View File

@ -714,7 +714,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
dependencies.storage.writeAsync { db in dependencies.storage.writeAsync { db in
try selectedUsers.forEach { userId in try selectedUsers.forEach { userId in
let thread: SessionThread = try SessionThread.fetchOrCreate(db, id: userId, variant: .contact) let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: userId, variant: .contact, shouldBeVisible: nil)
try LinkPreview( try LinkPreview(
url: communityUrl, url: communityUrl,

View File

@ -770,20 +770,13 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
// Delay the change to give the cell "unswipe" animation some time to complete // Delay the change to give the cell "unswipe" animation some time to complete
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) { DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) {
Storage.shared.writeAsync { db in Storage.shared.writeAsync { db in
// If we are unpinning then just clear the value try SessionThread
guard threadViewModel.threadPinnedPriority == 0 else { .filter(id: threadViewModel.threadId)
try SessionThread .updateAllAndConfig(
.filter(id: threadViewModel.threadId) db,
.updateAllAndConfig( SessionThread.Columns.pinnedPriority
db, .set(to: (threadViewModel.threadPinnedPriority == 0 ? 1 : 0))
SessionThread.Columns.pinnedPriority.set(to: 0) )
)
return
}
// Otherwise we want to reset the priority values for all of the currently
// pinned threads (adding the newly pinned one at the end)
try SessionThread.refreshPinnedPriorities(db, adding: threadViewModel.threadId)
} }
} }
} }

View File

@ -368,6 +368,10 @@ public class HomeViewModel {
public func delete(threadId: String, threadVariant: SessionThread.Variant) { public func delete(threadId: String, threadVariant: SessionThread.Variant) {
Storage.shared.writeAsync { db in Storage.shared.writeAsync { db in
switch threadVariant { switch threadVariant {
case .contact:
try SessionUtil
.hide(db, contactIds: [threadId])
case .legacyGroup, .group: case .legacyGroup, .group:
MessageSender MessageSender
.leave(db, groupPublicKey: threadId) .leave(db, groupPublicKey: threadId)
@ -379,8 +383,6 @@ public class HomeViewModel {
openGroupId: threadId, openGroupId: threadId,
calledFromConfigHandling: false calledFromConfigHandling: false
) )
default: break
} }
_ = try SessionThread _ = try SessionThread

View File

@ -179,7 +179,8 @@ final class NewConversationVC: BaseVC, ThemedNavigation, UITableViewDelegate, UI
let sessionId = newConversationViewModel.sectionData[indexPath.section].contacts[indexPath.row].id let sessionId = newConversationViewModel.sectionData[indexPath.section].contacts[indexPath.row].id
let maybeThread: SessionThread? = Storage.shared.write { db in let maybeThread: SessionThread? = Storage.shared.write { db in
try SessionThread.fetchOrCreate(db, id: sessionId, variant: .contact) try SessionThread
.fetchOrCreate(db, id: sessionId, variant: .contact, shouldBeVisible: nil)
} }
guard maybeThread != nil else { return } guard maybeThread != nil else { return }

View File

@ -233,7 +233,8 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle
private func startNewDM(with sessionId: String) { private func startNewDM(with sessionId: String) {
let maybeThread: SessionThread? = Storage.shared.write { db in let maybeThread: SessionThread? = Storage.shared.write { db in
try SessionThread.fetchOrCreate(db, id: sessionId, variant: .contact) try SessionThread
.fetchOrCreate(db, id: sessionId, variant: .contact, shouldBeVisible: nil)
} }
guard maybeThread != nil else { return } guard maybeThread != nil else { return }

View File

@ -12,7 +12,8 @@ public struct SessionApp {
public static func presentConversation(for threadId: String, action: ConversationViewModel.Action = .none, animated: Bool) { public static func presentConversation(for threadId: String, action: ConversationViewModel.Action = .none, animated: Bool) {
let maybeThreadInfo: (thread: SessionThread, isMessageRequest: Bool)? = Storage.shared.write { db in let maybeThreadInfo: (thread: SessionThread, isMessageRequest: Bool)? = Storage.shared.write { db in
let thread: SessionThread = try SessionThread.fetchOrCreate(db, id: threadId, variant: .contact) let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: threadId, variant: .contact, shouldBeVisible: nil)
return (thread, thread.isMessageRequest(db)) return (thread, thread.isMessageRequest(db))
} }

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -613,3 +613,4 @@
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
"MARK_AS_READ" = "Mark Read"; "MARK_AS_READ" = "Mark Read";
"MARK_AS_UNREAD" = "Mark Unread"; "MARK_AS_UNREAD" = "Mark Unread";
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";

View File

@ -304,7 +304,8 @@ public enum PushRegistrationError: Error {
}() }()
let call: SessionCall = SessionCall(db, for: caller, uuid: uuid, mode: .answer) let call: SessionCall = SessionCall(db, for: caller, uuid: uuid, mode: .answer)
let thread: SessionThread = try SessionThread.fetchOrCreate(db, id: caller, variant: .contact) let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: caller, variant: .contact, shouldBeVisible: nil)
let interaction: Interaction = try Interaction( let interaction: Interaction = try Interaction(
messageUuid: uuid, messageUuid: uuid,

View File

@ -151,8 +151,7 @@ enum Onboarding {
// Create the 'Note to Self' thread (not visible by default) // Create the 'Note to Self' thread (not visible by default)
try SessionThread try SessionThread
.fetchOrCreate(db, id: x25519PublicKey, variant: .contact) .fetchOrCreate(db, id: x25519PublicKey, variant: .contact, shouldBeVisible: false)
.save(db)
} }
// Set hasSyncedInitialConfiguration to true so that when we hit the // Set hasSyncedInitialConfiguration to true so that when we hit the

View File

@ -139,7 +139,8 @@ final class QRCodeVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControl
} }
else { else {
let maybeThread: SessionThread? = Storage.shared.write { db in let maybeThread: SessionThread? = Storage.shared.write { db in
try SessionThread.fetchOrCreate(db, id: hexEncodedPublicKey, variant: .contact) try SessionThread
.fetchOrCreate(db, id: hexEncodedPublicKey, variant: .contact, shouldBeVisible: nil)
} }
guard maybeThread != nil else { return } guard maybeThread != nil else { return }

View File

@ -108,7 +108,8 @@ enum MockDataGenerator {
logProgress("", "Start") logProgress("", "Start")
// First create the thread used to indicate that the mock data has been generated // First create the thread used to indicate that the mock data has been generated
_ = try? SessionThread.fetchOrCreate(db, id: "MockDatabaseThread", variant: .contact) _ = try? SessionThread
.fetchOrCreate(db, id: "MockDatabaseThread", variant: .contact, shouldBeVisible: false)
// MARK: - -- DM Thread // MARK: - -- DM Thread
@ -133,9 +134,12 @@ enum MockDataGenerator {
// Generate the thread // Generate the thread
let thread: SessionThread = try! SessionThread let thread: SessionThread = try! SessionThread
.fetchOrCreate(db, id: randomSessionId, variant: .contact) .fetchOrCreate(
.with(shouldBeVisible: true) db,
.saved(db) id: randomSessionId,
variant: .contact,
shouldBeVisible: true
)
// Generate the contact // Generate the contact
let contact: Contact = try! Contact( let contact: Contact = try! Contact(
@ -241,9 +245,12 @@ enum MockDataGenerator {
} }
let thread: SessionThread = try! SessionThread let thread: SessionThread = try! SessionThread
.fetchOrCreate(db, id: randomGroupPublicKey, variant: .legacyGroup) .fetchOrCreate(
.with(shouldBeVisible: true) db,
.saved(db) id: randomGroupPublicKey,
variant: .legacyGroup,
shouldBeVisible: true
)
_ = try! ClosedGroup( _ = try! ClosedGroup(
threadId: randomGroupPublicKey, threadId: randomGroupPublicKey,
name: groupName, name: groupName,
@ -367,9 +374,12 @@ enum MockDataGenerator {
// Create the open group model and the thread // Create the open group model and the thread
let thread: SessionThread = try! SessionThread let thread: SessionThread = try! SessionThread
.fetchOrCreate(db, id: randomGroupPublicKey, variant: .community) .fetchOrCreate(
.with(shouldBeVisible: true) db,
.saved(db) id: randomGroupPublicKey,
variant: .community,
shouldBeVisible: true
)
_ = try! OpenGroup( _ = try! OpenGroup(
server: serverName, server: serverName,
roomToken: roomName, roomToken: roomName,

View File

@ -179,12 +179,10 @@ public enum SMKLegacy {
internal func toNonLegacy(_ instance: Message? = nil) throws -> Message { internal func toNonLegacy(_ instance: Message? = nil) throws -> Message {
let result: Message = (instance ?? Message()) let result: Message = (instance ?? Message())
result.id = self.id result.id = self.id
result.threadId = self.threadID
result.sentTimestamp = self.sentTimestamp result.sentTimestamp = self.sentTimestamp
result.receivedTimestamp = self.receivedTimestamp result.receivedTimestamp = self.receivedTimestamp
result.recipient = self.recipient result.recipient = self.recipient
result.sender = self.sender result.sender = self.sender
result.groupPublicKey = self.groupPublicKey
result.openGroupServerMessageId = self.openGroupServerMessageID result.openGroupServerMessageId = self.openGroupServerMessageID
result.serverHash = self.serverHash result.serverHash = self.serverHash

View File

@ -57,12 +57,9 @@ enum _012_SharedUtilChanges: Migration {
// MARK: - Shared Data // MARK: - Shared Data
let pinnedThreadIds: [String] = try SessionThread let allThreads: [String: SessionThread] = try SessionThread
.select(SessionThread.Columns.id)
.filter(SessionThread.Columns.isPinned)
.order(Column.rowID)
.asRequest(of: String.self)
.fetchAll(db) .fetchAll(db)
.reduce(into: [:]) { result, next in result[next.id] = next }
// MARK: - UserProfile Config Dump // MARK: - UserProfile Config Dump
@ -71,12 +68,12 @@ enum _012_SharedUtilChanges: Migration {
secretKey: secretKey, secretKey: secretKey,
cachedData: nil cachedData: nil
) )
let userProfileConfResult: SessionUtil.ConfResult = try SessionUtil.update( try SessionUtil.update(
profile: Profile.fetchOrCreateCurrentUser(db), profile: Profile.fetchOrCreateCurrentUser(db),
in: userProfileConf in: userProfileConf
) )
if userProfileConfResult.needsDump { if config_needs_dump(userProfileConf) {
try SessionUtil try SessionUtil
.createDump( .createDump(
conf: userProfileConf, conf: userProfileConf,
@ -89,6 +86,10 @@ enum _012_SharedUtilChanges: Migration {
// MARK: - Contact Config Dump // MARK: - Contact Config Dump
let contactsData: [ContactInfo] = try Contact let contactsData: [ContactInfo] = try Contact
.filter(
Contact.Columns.isBlocked == true ||
allThreads.keys.contains(Contact.Columns.id)
)
.including(optional: Contact.profile) .including(optional: Contact.profile)
.asRequest(of: ContactInfo.self) .asRequest(of: ContactInfo.self)
.fetchAll(db) .fetchAll(db)
@ -98,21 +99,21 @@ enum _012_SharedUtilChanges: Migration {
secretKey: secretKey, secretKey: secretKey,
cachedData: nil cachedData: nil
) )
let contactsConfResult: SessionUtil.ConfResult = try SessionUtil.upsert( try SessionUtil.upsert(
contactData: contactsData contactData: contactsData
.map { data in .map { data in
( SessionUtil.SyncedContactInfo(
data.contact.id, id: data.contact.id,
data.contact, contact: data.contact,
data.profile, profile: data.profile,
Int32(pinnedThreadIds.firstIndex(of: data.contact.id) ?? 0), priority: Int32(allThreads[data.contact.id]?.pinnedPriority ?? 0),
false hidden: (allThreads[data.contact.id]?.shouldBeVisible == true)
) )
}, },
in: contactsConf in: contactsConf
) )
if contactsConfResult.needsDump { if config_needs_dump(contactsConf) {
try SessionUtil try SessionUtil
.createDump( .createDump(
conf: contactsConf, conf: contactsConf,
@ -130,15 +131,15 @@ enum _012_SharedUtilChanges: Migration {
secretKey: secretKey, secretKey: secretKey,
cachedData: nil cachedData: nil
) )
let convoInfoVolatileConfResult: SessionUtil.ConfResult = try SessionUtil.upsert( try SessionUtil.upsert(
convoInfoVolatileChanges: volatileThreadInfo, convoInfoVolatileChanges: volatileThreadInfo,
in: convoInfoVolatileConf in: convoInfoVolatileConf
) )
if convoInfoVolatileConfResult.needsDump { if config_needs_dump(convoInfoVolatileConf) {
try SessionUtil try SessionUtil
.createDump( .createDump(
conf: contactsConf, conf: convoInfoVolatileConf,
for: .convoInfoVolatile, for: .convoInfoVolatile,
publicKey: userPublicKey publicKey: userPublicKey
)? )?
@ -155,16 +156,17 @@ enum _012_SharedUtilChanges: Migration {
secretKey: secretKey, secretKey: secretKey,
cachedData: nil cachedData: nil
) )
let userGroupConfResult1: SessionUtil.ConfResult = try SessionUtil.upsert( try SessionUtil.upsert(
legacyGroups: legacyGroupData, legacyGroups: legacyGroupData,
in: userGroupsConf in: userGroupsConf
) )
let userGroupConfResult2: SessionUtil.ConfResult = try SessionUtil.upsert( try SessionUtil.upsert(
communities: communityData.map { ($0, nil) }, communities: communityData
.map { SessionUtil.CommunityInfo(urlInfo: $0) },
in: userGroupsConf in: userGroupsConf
) )
if userGroupConfResult1.needsDump || userGroupConfResult2.needsDump { if config_needs_dump(userGroupsConf) {
try SessionUtil try SessionUtil
.createDump( .createDump(
conf: userGroupsConf, conf: userGroupsConf,
@ -174,33 +176,18 @@ enum _012_SharedUtilChanges: Migration {
.save(db) .save(db)
} }
// MARK: - Pinned thread priorities // MARK: - Threads
struct PinnedTeadInfo: Decodable, FetchableRecord { try SessionUtil
let id: String .updatingThreads(db, Array(allThreads.values))
let creationDateTimestamp: TimeInterval
let maxInteractionTimestampMs: Int64? // MARK: - Syncing
var targetTimestamp: Int64 { // Enqueue a config sync job to ensure the generated configs get synced
(maxInteractionTimestampMs ?? Int64(creationDateTimestamp * 1000)) db.afterNextTransactionNestedOnce(dedupeId: SessionUtil.syncDedupeId(userPublicKey)) { db in
} ConfigurationSyncJob.enqueue(db, publicKey: userPublicKey)
} }
// At the time of writing the thread sorting was 'pinned (flag), most recent interaction
// timestamp, thread creation timestamp)
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let pinnedThreads: [PinnedTeadInfo] = try SessionThread
.select(.id, .creationDateTimestamp)
.filter(SessionThread.Columns.isPinned == true)
.annotated(with: SessionThread.interactions.max(Interaction.Columns.timestampMs))
.asRequest(of: PinnedTeadInfo.self)
.fetchAll(db)
.sorted { lhs, rhs in lhs.targetTimestamp > rhs.targetTimestamp }
// Update the pinned thread priorities
try SessionUtil
.updateThreadPrioritiesIfNeeded(db, [SessionThread.Columns.pinnedPriority.set(to: 0)], [])
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
} }

View File

@ -146,43 +146,50 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord,
} }
} }
// MARK: - Mutation
public extension SessionThread {
func with(
shouldBeVisible: Bool? = nil,
pinnedPriority: Int32? = nil
) -> SessionThread {
return SessionThread(
id: id,
variant: variant,
creationDateTimestamp: creationDateTimestamp,
shouldBeVisible: (shouldBeVisible ?? self.shouldBeVisible),
messageDraft: messageDraft,
notificationSound: notificationSound,
mutedUntilTimestamp: mutedUntilTimestamp,
onlyNotifyForMentions: onlyNotifyForMentions,
markedAsUnread: markedAsUnread,
pinnedPriority: (pinnedPriority ?? self.pinnedPriority)
)
}
}
// MARK: - GRDB Interactions // MARK: - GRDB Interactions
public extension SessionThread { public extension SessionThread {
/// Fetches or creates a SessionThread with the specified id and variant /// Fetches or creates a SessionThread with the specified id, variant and visible state
/// ///
/// **Notes:** /// **Notes:**
/// - The `variant` will be ignored if an existing thread is found /// - The `variant` will be ignored if an existing thread is found
/// - This method **will** save the newly created SessionThread to the database /// - This method **will** save the newly created SessionThread to the database
static func fetchOrCreate(_ db: Database, id: ID, variant: Variant) throws -> SessionThread { @discardableResult static func fetchOrCreate(
_ db: Database,
id: ID,
variant: Variant,
shouldBeVisible: Bool?
) throws -> SessionThread {
guard let existingThread: SessionThread = try? fetchOne(db, id: id) else { guard let existingThread: SessionThread = try? fetchOne(db, id: id) else {
return try SessionThread(id: id, variant: variant) return try SessionThread(
.saved(db) id: id,
variant: variant,
shouldBeVisible: (shouldBeVisible ?? false)
).saved(db)
} }
return existingThread // If the `shouldBeVisible` state matches then we can finish early
guard
let desiredVisibility: Bool = shouldBeVisible,
existingThread.shouldBeVisible != desiredVisibility
else { return existingThread }
// Update the `shouldBeVisible` state
try SessionThread
.filter(id: id)
.updateAllAndConfig(
db,
SessionThread.Columns.shouldBeVisible.set(to: shouldBeVisible)
)
// Retrieve the updated thread and return it (we don't recursively call this method
// just in case something weird happened and the above update didn't work, as that
// would result in an infinite loop)
return (try fetchOne(db, id: id))
.defaulting(
to: try SessionThread(id: id, variant: variant, shouldBeVisible: desiredVisibility)
.saved(db)
)
} }
func isMessageRequest(_ db: Database, includeNonVisible: Bool = false) -> Bool { func isMessageRequest(_ db: Database, includeNonVisible: Bool = false) -> Bool {
@ -199,6 +206,7 @@ public extension SessionThread {
) )
} }
@available(*, unavailable, message: "should not be used until pin re-ordering is built")
static func refreshPinnedPriorities(_ db: Database, adding threadId: String) throws { static func refreshPinnedPriorities(_ db: Database, adding threadId: String) throws {
struct PinnedPriority: TableRecord, ColumnExpressible { struct PinnedPriority: TableRecord, ColumnExpressible {
public typealias Columns = CodingKeys public typealias Columns = CodingKeys

View File

@ -17,6 +17,7 @@ public enum MessageReceiveJob: JobExecutor {
deferred: @escaping (Job) -> () deferred: @escaping (Job) -> ()
) { ) {
guard guard
let threadId: String = job.threadId,
let detailsData: Data = job.details, let detailsData: Data = job.details,
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData) let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
else { else {
@ -59,10 +60,11 @@ public enum MessageReceiveJob: JobExecutor {
do { do {
try MessageReceiver.handle( try MessageReceiver.handle(
db, db,
threadId: threadId,
threadVariant: messageInfo.threadVariant,
message: messageInfo.message, message: messageInfo.message,
serverExpirationTimestamp: messageInfo.serverExpirationTimestamp, serverExpirationTimestamp: messageInfo.serverExpirationTimestamp,
associatedWithProto: protoContent, associatedWithProto: protoContent
openGroupId: nil
) )
} }
catch { catch {
@ -131,23 +133,27 @@ extension MessageReceiveJob {
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case message case message
case variant case variant
case threadVariant
case serverExpirationTimestamp case serverExpirationTimestamp
case serializedProtoData case serializedProtoData
} }
public let message: Message public let message: Message
public let variant: Message.Variant public let variant: Message.Variant
public let threadVariant: SessionThread.Variant
public let serverExpirationTimestamp: TimeInterval? public let serverExpirationTimestamp: TimeInterval?
public let serializedProtoData: Data public let serializedProtoData: Data
public init( public init(
message: Message, message: Message,
variant: Message.Variant, variant: Message.Variant,
threadVariant: SessionThread.Variant,
serverExpirationTimestamp: TimeInterval?, serverExpirationTimestamp: TimeInterval?,
proto: SNProtoContent proto: SNProtoContent
) throws { ) throws {
self.message = message self.message = message
self.variant = variant self.variant = variant
self.threadVariant = threadVariant
self.serverExpirationTimestamp = serverExpirationTimestamp self.serverExpirationTimestamp = serverExpirationTimestamp
self.serializedProtoData = try proto.serializedData() self.serializedProtoData = try proto.serializedData()
} }
@ -155,11 +161,13 @@ extension MessageReceiveJob {
private init( private init(
message: Message, message: Message,
variant: Message.Variant, variant: Message.Variant,
threadVariant: SessionThread.Variant,
serverExpirationTimestamp: TimeInterval?, serverExpirationTimestamp: TimeInterval?,
serializedProtoData: Data serializedProtoData: Data
) { ) {
self.message = message self.message = message
self.variant = variant self.variant = variant
self.threadVariant = threadVariant
self.serverExpirationTimestamp = serverExpirationTimestamp self.serverExpirationTimestamp = serverExpirationTimestamp
self.serializedProtoData = serializedProtoData self.serializedProtoData = serializedProtoData
} }
@ -177,6 +185,24 @@ extension MessageReceiveJob {
self = MessageInfo( self = MessageInfo(
message: try variant.decode(from: container, forKey: .message), message: try variant.decode(from: container, forKey: .message),
variant: variant, variant: variant,
threadVariant: (try? container.decode(SessionThread.Variant.self, forKey: .threadVariant))
.defaulting(to: {
/// We used to store a 'groupPublicKey' value within the 'Message' type which was used to
/// determine the thread variant, now we just encode the variant directly but there may be
/// some legacy jobs which still have `groupPublicKey` so we have this mechanism
///
/// **Note:** This can probably be removed a couple of releases after the user config
/// update release (ie. after June 2023)
class LegacyGroupPubkey: Codable {
let groupPublicKey: String?
}
if (try? container.decode(LegacyGroupPubkey.self, forKey: .message))?.groupPublicKey != nil {
return .legacyGroup
}
return .contact
}()),
serverExpirationTimestamp: try? container.decode(TimeInterval.self, forKey: .serverExpirationTimestamp), serverExpirationTimestamp: try? container.decode(TimeInterval.self, forKey: .serverExpirationTimestamp),
serializedProtoData: try container.decode(Data.self, forKey: .serializedProtoData) serializedProtoData: try container.decode(Data.self, forKey: .serializedProtoData)
) )
@ -192,6 +218,7 @@ extension MessageReceiveJob {
try container.encode(message, forKey: .message) try container.encode(message, forKey: .message)
try container.encode(variant, forKey: .variant) try container.encode(variant, forKey: .variant)
try container.encode(threadVariant, forKey: .threadVariant)
try container.encodeIfPresent(serverExpirationTimestamp, forKey: .serverExpirationTimestamp) try container.encodeIfPresent(serverExpirationTimestamp, forKey: .serverExpirationTimestamp)
try container.encode(serializedProtoData, forKey: .serializedProtoData) try container.encode(serializedProtoData, forKey: .serializedProtoData)
} }

View File

@ -157,9 +157,6 @@ public enum MessageSendJob: JobExecutor {
// Store the sentTimestamp from the message in case it fails due to a clockOutOfSync error // Store the sentTimestamp from the message in case it fails due to a clockOutOfSync error
let originalSentTimestamp: UInt64? = details.message.sentTimestamp let originalSentTimestamp: UInt64? = details.message.sentTimestamp
// Add the threadId to the message if there isn't one set
details.message.threadId = (details.message.threadId ?? job.threadId)
/// Perform the actual message sending /// Perform the actual message sending
/// ///
/// **Note:** No need to upload attachments as part of this process as the above logic splits that out into it's own job /// **Note:** No need to upload attachments as part of this process as the above logic splits that out into it's own job

View File

@ -6,6 +6,16 @@ import SessionUtil
import SessionUtilitiesKit import SessionUtilitiesKit
internal extension SessionUtil { internal extension SessionUtil {
static let columnsRelatedToContacts: [ColumnExpression] = [
Contact.Columns.isApproved,
Contact.Columns.isBlocked,
Contact.Columns.didApproveMe,
Profile.Columns.name,
Profile.Columns.nickname,
Profile.Columns.profilePictureUrl,
Profile.Columns.profileEncryptionKey
]
// MARK: - Incoming Changes // MARK: - Incoming Changes
static func handleContactsUpdate( static func handleContactsUpdate(
@ -56,6 +66,7 @@ internal extension SessionUtil {
contactData[contactId] = ( contactData[contactId] = (
contactResult, contactResult,
profileResult, profileResult,
contact.hidden
) )
contacts_iterator_advance(contactIterator) contacts_iterator_advance(contactIterator)
} }
@ -82,9 +93,9 @@ internal extension SessionUtil {
if if
(!data.profile.name.isEmpty && profile.name != data.profile.name) || (!data.profile.name.isEmpty && profile.name != data.profile.name) ||
profile.nickname != data.profile.nickname || profile.nickname != data.profile.nickname ||
profile.profilePictureUrl != data.profile.profilePictureUrl || profile.profilePictureUrl != data.profile.profilePictureUrl ||
profile.profileEncryptionKey != data.profile.profileEncryptionKey profile.profileEncryptionKey != data.profile.profileEncryptionKey
{ {
try profile.save(db) try profile.save(db)
try Profile try Profile
@ -138,47 +149,59 @@ internal extension SessionUtil {
/// If the contact's `hidden` flag doesn't match the visibility of their conversation then create/delete the /// If the contact's `hidden` flag doesn't match the visibility of their conversation then create/delete the
/// associated contact conversation accordingly /// associated contact conversation accordingly
let threadExists: Bool = try SessionThread.exists(db, id: contact.id) let threadExists: Bool = try SessionThread.exists(db, id: contact.id)
let threadIsVisible: Bool = try SessionThread
.filter(id: contact.id)
.select(.shouldBeVisible)
.asRequest(of: Bool.self)
.fetchOne(db)
.defaulting(to: false)
if data.isHiddenConversation && threadExists { switch (data.isHiddenConversation, threadExists, threadIsVisible) {
try SessionThread case (true, true, _):
.deleteOne(db, id: contact.id) try SessionThread
} .filter(id: contact.id)
else if !data.isHiddenConversation && !threadExists { .deleteAll(db)
try SessionThread(id: contact.id, variant: .contact)
.save(db) case (false, false, _):
try SessionThread(
id: contact.id,
variant: .contact,
shouldBeVisible: true
).save(db)
case (false, true, false):
try SessionThread
.filter(id: contact.id)
.updateAll( // Handling a config update so don't use `updateAllAndConfig`
db,
SessionThread.Columns.shouldBeVisible.set(to: !data.isHiddenConversation)
)
default: break
} }
} }
} }
// MARK: - Outgoing Changes // MARK: - Outgoing Changes
typealias ContactData = (
id: String,
contact: Contact?,
profile: Profile?,
priority: Int32?,
hidden: Bool?
)
static func upsert( static func upsert(
contactData: [ContactData], contactData: [SyncedContactInfo],
in conf: UnsafeMutablePointer<config_object>? in conf: UnsafeMutablePointer<config_object>?
) throws -> ConfResult { ) throws {
guard conf != nil else { throw SessionUtilError.nilConfigObject } 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
let userPublicKey: String = getUserHexEncodedPublicKey() let userPublicKey: String = getUserHexEncodedPublicKey()
let targetContacts: [SyncedContactInfo] = contactData
.filter { $0.id != userPublicKey } .filter { $0.id != userPublicKey }
let targetContacts: [(id: String, contact: Contact?, profile: Profile?, priority: Int32?, hidden: Bool?)] = contactData
// If we only updated the current user contact then no need to continue // If we only updated the current user contact then no need to continue
guard !targetContacts.isEmpty else { return ConfResult(needsPush: false, needsDump: false) } guard !targetContacts.isEmpty else { return }
// Update the name // Update the name
targetContacts targetContacts
.forEach { (id, maybeContact, maybeProfile, priority, hidden) in .forEach { info in
var sessionId: [CChar] = id.cArray var sessionId: [CChar] = info.id.cArray
var contact: contacts_contact = contacts_contact() var contact: contacts_contact = contacts_contact()
guard contacts_get_or_construct(conf, &contact, &sessionId) else { guard contacts_get_or_construct(conf, &contact, &sessionId) else {
SNLog("Unable to upsert contact from Config Message") SNLog("Unable to upsert contact from Config Message")
@ -186,7 +209,7 @@ internal extension SessionUtil {
} }
// Assign all properties to match the updated contact (if there is one) // Assign all properties to match the updated contact (if there is one)
if let updatedContact: Contact = maybeContact { if let updatedContact: Contact = info.contact {
contact.approved = updatedContact.isApproved contact.approved = updatedContact.isApproved
contact.approved_me = updatedContact.didApproveMe contact.approved_me = updatedContact.didApproveMe
contact.blocked = updatedContact.isBlocked contact.blocked = updatedContact.isBlocked
@ -197,7 +220,7 @@ internal extension SessionUtil {
// Update the profile data (if there is one - users we have sent a message request to may // Update the profile data (if there is one - users we have sent a message request to may
// not have profile info in certain situations) // not have profile info in certain situations)
if let updatedProfile: Profile = maybeProfile { if let updatedProfile: Profile = info.profile {
let oldAvatarUrl: String? = String(libSessionVal: contact.profile_pic.url) let oldAvatarUrl: String? = String(libSessionVal: contact.profile_pic.url)
let oldAvatarKey: Data? = Data( let oldAvatarKey: Data? = Data(
libSessionVal: contact.profile_pic.key, libSessionVal: contact.profile_pic.key,
@ -209,9 +232,13 @@ internal extension SessionUtil {
contact.profile_pic.url = updatedProfile.profilePictureUrl.toLibSession() contact.profile_pic.url = updatedProfile.profilePictureUrl.toLibSession()
contact.profile_pic.key = updatedProfile.profileEncryptionKey.toLibSession() contact.profile_pic.key = updatedProfile.profileEncryptionKey.toLibSession()
// Download the profile picture if needed // Download the profile picture if needed (this can be triggered within
// database reads/writes so dispatch the download to a separate queue to
// prevent blocking)
if oldAvatarUrl != updatedProfile.profilePictureUrl || oldAvatarKey != updatedProfile.profileEncryptionKey { if oldAvatarUrl != updatedProfile.profilePictureUrl || oldAvatarKey != updatedProfile.profileEncryptionKey {
ProfileManager.downloadAvatar(for: updatedProfile) DispatchQueue.global(qos: .background).async {
ProfileManager.downloadAvatar(for: updatedProfile)
}
} }
// Store the updated contact (needs to happen before variables go out of scope) // Store the updated contact (needs to happen before variables go out of scope)
@ -219,15 +246,29 @@ internal extension SessionUtil {
} }
// Store the updated contact (can't be sure if we made any changes above) // Store the updated contact (can't be sure if we made any changes above)
contact.hidden = (hidden ?? contact.hidden) contact.hidden = (info.hidden ?? contact.hidden)
contact.priority = (priority ?? contact.priority) contact.priority = (info.priority ?? contact.priority)
contacts_set(conf, &contact) contacts_set(conf, &contact)
} }
}
return ConfResult( }
needsPush: config_needs_push(conf),
needsDump: config_needs_dump(conf) // MARK: - External Outgoing Changes
)
public extension SessionUtil {
static func hide(_ db: Database, contactIds: [String]) throws {
try SessionUtil.performAndPushChange(
db,
for: .contacts,
publicKey: getUserHexEncodedPublicKey(db)
) { conf in
// Mark the contacts as hidden
try SessionUtil.upsert(
contactData: contactIds
.map { SyncedContactInfo(id: $0, hidden: true) },
in: conf
)
}
} }
} }
@ -245,28 +286,43 @@ internal extension SessionUtil {
guard !targetContacts.isEmpty else { return updated } guard !targetContacts.isEmpty else { return updated }
do { do {
let atomicConf: Atomic<UnsafeMutablePointer<config_object>?> = SessionUtil.config( try SessionUtil.performAndPushChange(
db,
for: .contacts, for: .contacts,
publicKey: userPublicKey publicKey: userPublicKey
) ) { conf in
// When inserting new contacts (or contacts with invalid profile data) we want
// Since we are doing direct memory manipulation we are using an `Atomic` type which has // to add any valid profile information we have so identify if any of the updated
// blocking access in it's `mutate` closure // contacts are new/invalid, and if so, fetch any profile data we have for them
try atomicConf.mutate { conf in let newContactIds: [String] = targetContacts
let result: ConfResult = try SessionUtil .compactMap { contactData -> String? in
var cContactId: [CChar] = contactData.id.cArray
var contact: contacts_contact = contacts_contact()
guard
contacts_get(conf, &contact, &cContactId),
String(libSessionVal: contact.name, nullIfEmpty: true) != nil
else { return contactData.id }
return nil
}
let newProfiles: [String: Profile] = try Profile
.fetchAll(db, ids: newContactIds)
.reduce(into: [:]) { result, next in result[next.id] = next }
// Upsert the updated contact data
try SessionUtil
.upsert( .upsert(
contactData: targetContacts.map { ($0.id, $0, nil, nil, nil) }, contactData: targetContacts
.map { contact in
SyncedContactInfo(
id: contact.id,
contact: contact,
profile: newProfiles[contact.id]
)
},
in: conf in: conf
) )
// If we don't need to dump the data the we can finish early
guard result.needsDump else { return }
try SessionUtil.createDump(
conf: conf,
for: .contacts,
publicKey: userPublicKey
)?.save(db)
} }
} }
catch { catch {
@ -304,52 +360,30 @@ internal extension SessionUtil {
do { do {
// Update the user profile first (if needed) // Update the user profile first (if needed)
if let updatedUserProfile: Profile = updatedProfiles.first(where: { $0.id == userPublicKey }) { if let updatedUserProfile: Profile = updatedProfiles.first(where: { $0.id == userPublicKey }) {
try SessionUtil try SessionUtil.performAndPushChange(
.config( db,
for: .userProfile, for: .userProfile,
publicKey: userPublicKey publicKey: userPublicKey
) { conf in
try SessionUtil.update(
profile: updatedUserProfile,
in: conf
) )
.mutate { conf in }
let result: ConfResult = try SessionUtil.update(
profile: updatedUserProfile,
in: conf
)
// If we don't need to dump the data the we can finish early
guard result.needsDump else { return }
try SessionUtil.createDump(
conf: conf,
for: .userProfile,
publicKey: userPublicKey
)?.save(db)
}
} }
// Since we are doing direct memory manipulation we are using an `Atomic` type which has try SessionUtil.performAndPushChange(
// blocking access in it's `mutate` closure db,
try SessionUtil for: .contacts,
.config( publicKey: userPublicKey
for: .contacts, ) { conf in
publicKey: userPublicKey try SessionUtil
) .upsert(
.mutate { conf in contactData: targetProfiles
let result: ConfResult = try SessionUtil .map { SyncedContactInfo(id: $0.id, profile: $0) },
.upsert( in: conf
contactData: targetProfiles )
.map { ($0.id, nil, $0, nil, nil) }, }
in: conf
)
// If we don't need to dump the data the we can finish early
guard result.needsDump else { return }
try SessionUtil.createDump(
conf: conf,
for: .contacts,
publicKey: userPublicKey
)?.save(db)
}
} }
catch { catch {
SNLog("[libSession-util] Failed to dump updated data") SNLog("[libSession-util] Failed to dump updated data")
@ -358,3 +392,29 @@ internal extension SessionUtil {
return updated return updated
} }
} }
// MARK: - SyncedContactInfo
extension SessionUtil {
struct SyncedContactInfo {
let id: String
let contact: Contact?
let profile: Profile?
let priority: Int32?
let hidden: Bool?
init(
id: String,
contact: Contact? = nil,
profile: Profile? = nil,
priority: Int32? = nil,
hidden: Bool? = nil
) {
self.id = id
self.contact = contact
self.profile = profile
self.priority = priority
self.hidden = hidden
}
}
}

View File

@ -158,16 +158,22 @@ internal extension SessionUtil {
// If there are no newer local last read timestamps then just return the mergeResult // If there are no newer local last read timestamps then just return the mergeResult
guard !newerLocalChanges.isEmpty else { return } guard !newerLocalChanges.isEmpty else { return }
try upsert( try SessionUtil.performAndPushChange(
convoInfoVolatileChanges: newerLocalChanges, db,
in: conf for: .convoInfoVolatile,
) publicKey: getUserHexEncodedPublicKey(db)
) { conf in
try upsert(
convoInfoVolatileChanges: newerLocalChanges,
in: conf
)
}
} }
@discardableResult static func upsert( static func upsert(
convoInfoVolatileChanges: [VolatileThreadInfo], convoInfoVolatileChanges: [VolatileThreadInfo],
in conf: UnsafeMutablePointer<config_object>? in conf: UnsafeMutablePointer<config_object>?
) throws -> ConfResult { ) throws {
guard conf != nil else { throw SessionUtilError.nilConfigObject } guard conf != nil else { throw SessionUtilError.nilConfigObject }
convoInfoVolatileChanges.forEach { threadInfo in convoInfoVolatileChanges.forEach { threadInfo in
@ -240,30 +246,23 @@ internal extension SessionUtil {
} }
convo_info_volatile_set_community(conf, &community) convo_info_volatile_set_community(conf, &community)
case .group: return // TODO: Need to add when the type is added to the lib. case .group: return // TODO: Need to add when the type is added to the lib
} }
} }
return ConfResult(
needsPush: config_needs_push(conf),
needsDump: config_needs_dump(conf)
)
} }
} }
// MARK: - Convenience // MARK: - Convenience
internal extension SessionUtil { internal extension SessionUtil {
@discardableResult static func updatingThreadsConvoInfoVolatile<T>(_ db: Database, _ updated: [T]) throws -> [T] { static func updateMarkedAsUnreadState(
guard let updatedThreads: [SessionThread] = updated as? [SessionThread] else { _ db: Database,
throw StorageError.generic threads: [SessionThread]
} ) throws {
// If we have no updated threads then no need to continue // If we have no updated threads then no need to continue
guard !updatedThreads.isEmpty else { return updated } guard !threads.isEmpty else { return }
let userPublicKey: String = getUserHexEncodedPublicKey(db) let changes: [VolatileThreadInfo] = try threads.map { thread in
let changes: [VolatileThreadInfo] = try updatedThreads.map { thread in
VolatileThreadInfo( VolatileThreadInfo(
threadId: thread.id, threadId: thread.id,
variant: thread.variant, variant: thread.variant,
@ -273,36 +272,17 @@ internal extension SessionUtil {
changes: [.markedAsUnread(thread.markedAsUnread ?? false)] changes: [.markedAsUnread(thread.markedAsUnread ?? false)]
) )
} }
do { try SessionUtil.performAndPushChange(
try SessionUtil db,
.config( for: .convoInfoVolatile,
for: .convoInfoVolatile, publicKey: getUserHexEncodedPublicKey(db)
publicKey: userPublicKey ) { conf in
) try upsert(
.mutate { conf in convoInfoVolatileChanges: changes,
guard conf != nil else { throw SessionUtilError.nilConfigObject } in: conf
)
let result: ConfResult = try upsert(
convoInfoVolatileChanges: changes,
in: conf
)
// If we don't need to dump the data the we can finish early
guard result.needsDump else { return }
try SessionUtil.createDump(
conf: conf,
for: .convoInfoVolatile,
publicKey: userPublicKey
)?.save(db)
}
} }
catch {
SNLog("[libSession-util] Failed to dump updated data")
}
return updated
} }
static func syncThreadLastReadIfNeeded( static func syncThreadLastReadIfNeeded(
@ -311,7 +291,6 @@ internal extension SessionUtil {
threadVariant: SessionThread.Variant, threadVariant: SessionThread.Variant,
lastReadTimestampMs: Int64 lastReadTimestampMs: Int64
) throws { ) throws {
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let change: VolatileThreadInfo = VolatileThreadInfo( let change: VolatileThreadInfo = VolatileThreadInfo(
threadId: threadId, threadId: threadId,
variant: threadVariant, variant: threadVariant,
@ -321,36 +300,15 @@ internal extension SessionUtil {
changes: [.lastReadTimestampMs(lastReadTimestampMs)] changes: [.lastReadTimestampMs(lastReadTimestampMs)]
) )
let needsPush: Bool = try SessionUtil try SessionUtil.performAndPushChange(
.config( db,
for: .convoInfoVolatile, for: .convoInfoVolatile,
publicKey: userPublicKey publicKey: getUserHexEncodedPublicKey(db)
) { conf in
try upsert(
convoInfoVolatileChanges: [change],
in: conf
) )
.mutate { conf in
guard conf != nil else { throw SessionUtilError.nilConfigObject }
let result: ConfResult = try upsert(
convoInfoVolatileChanges: [change],
in: conf
)
// If we don't need to dump the data the we can finish early
guard result.needsDump else { return result.needsPush }
try SessionUtil.createDump(
conf: conf,
for: .contacts,
publicKey: userPublicKey
)?.save(db)
return result.needsPush
}
// If we need to push then enqueue a 'ConfigurationSyncJob'
guard needsPush else { return }
db.afterNextTransactionNestedOnce(dedupeId: SessionUtil.syncDedupeId(userPublicKey)) { db in
ConfigurationSyncJob.enqueue(db, publicKey: userPublicKey)
} }
} }
@ -361,51 +319,50 @@ internal extension SessionUtil {
userPublicKey: String, userPublicKey: String,
openGroup: OpenGroup? openGroup: OpenGroup?
) -> Bool { ) -> Bool {
let atomicConf: Atomic<UnsafeMutablePointer<config_object>?> = SessionUtil.config( return SessionUtil
for: .convoInfoVolatile, .config(
publicKey: userPublicKey for: .convoInfoVolatile,
) publicKey: userPublicKey
)
// If we don't have a config then just assume it's unread .wrappedValue
guard atomicConf.wrappedValue != nil else { return false } .map { conf in
switch threadVariant {
// Since we are doing direct memory manipulation we are using an `Atomic` type which has case .contact:
// blocking access in it's `mutate` closure var cThreadId: [CChar] = threadId.cArray
return atomicConf.mutate { conf in var oneToOne: convo_info_volatile_1to1 = convo_info_volatile_1to1()
switch threadVariant { guard convo_info_volatile_get_1to1(conf, &oneToOne, &cThreadId) else {
case .contact: return false
var cThreadId: [CChar] = threadId.cArray }
var oneToOne: convo_info_volatile_1to1 = convo_info_volatile_1to1()
guard convo_info_volatile_get_1to1(conf, &oneToOne, &cThreadId) else { return false } return (oneToOne.last_read > timestampMs)
return (oneToOne.last_read > timestampMs) case .legacyGroup:
var cThreadId: [CChar] = threadId.cArray
case .legacyGroup: var legacyGroup: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
var cThreadId: [CChar] = threadId.cArray
var legacyGroup: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() guard convo_info_volatile_get_legacy_group(conf, &legacyGroup, &cThreadId) else {
return false
guard convo_info_volatile_get_legacy_group(conf, &legacyGroup, &cThreadId) else { }
return false
} return (legacyGroup.last_read > timestampMs)
return (legacyGroup.last_read > timestampMs) case .community:
guard let openGroup: OpenGroup = openGroup else { return false }
case .community:
guard let openGroup: OpenGroup = openGroup else { return false } var cBaseUrl: [CChar] = openGroup.server.cArray
var cRoomToken: [CChar] = openGroup.roomToken.cArray
var cBaseUrl: [CChar] = openGroup.server.cArray var convoCommunity: convo_info_volatile_community = convo_info_volatile_community()
var cRoomToken: [CChar] = openGroup.roomToken.cArray
var convoCommunity: convo_info_volatile_community = convo_info_volatile_community() guard convo_info_volatile_get_community(conf, &convoCommunity, &cBaseUrl, &cRoomToken) else {
return false
guard convo_info_volatile_get_community(conf, &convoCommunity, &cBaseUrl, &cRoomToken) else { }
return false
} return (convoCommunity.last_read > timestampMs)
return (convoCommunity.last_read > timestampMs) case .group: return false // TODO: Need to add when the type is added to the lib
}
case .group: return false // TODO: Need to add when the type is added to the lib
} }
} .defaulting(to: false) // If we don't have a config then just assume it's unread
} }
} }

View File

@ -8,143 +8,172 @@ import SessionUtilitiesKit
// MARK: - Convenience // MARK: - Convenience
internal extension SessionUtil { internal extension SessionUtil {
static let columnsRelatedToThreads: [ColumnExpression] = [
SessionThread.Columns.pinnedPriority,
SessionThread.Columns.shouldBeVisible
]
static func assignmentsRequireConfigUpdate(_ assignments: [ConfigColumnAssignment]) -> Bool { static func assignmentsRequireConfigUpdate(_ assignments: [ConfigColumnAssignment]) -> Bool {
let targetColumns: Set<ColumnKey> = Set(assignments.map { ColumnKey($0.column) }) let targetColumns: Set<ColumnKey> = Set(assignments.map { ColumnKey($0.column) })
let allColumnsThatTriggerConfigUpdate: Set<ColumnKey> = [] let allColumnsThatTriggerConfigUpdate: Set<ColumnKey> = []
.appending(contentsOf: columnsRelatedToUserProfile) .appending(contentsOf: columnsRelatedToUserProfile)
.appending(contentsOf: columnsRelatedToContacts) .appending(contentsOf: columnsRelatedToContacts)
.appending(contentsOf: columnsRelatedToConvoInfoVolatile) .appending(contentsOf: columnsRelatedToConvoInfoVolatile)
.appending(contentsOf: columnsRelatedToUserGroups)
.map { ColumnKey($0) } .map { ColumnKey($0) }
.asSet() .asSet()
return !allColumnsThatTriggerConfigUpdate.isDisjoint(with: targetColumns) return !allColumnsThatTriggerConfigUpdate.isDisjoint(with: targetColumns)
} }
/// This function assumes that the `pinnedPriority` values get set correctly elsewhere rather than trying to enforce
/// uniqueness in here (this means if we eventually allow for "priority grouping" this logic wouldn't change - just where the static func performAndPushChange(
/// priorities get updated in the HomeVC
static func updateThreadPrioritiesIfNeeded<T>(
_ db: Database, _ db: Database,
_ assignments: [ConfigColumnAssignment], for variant: ConfigDump.Variant,
_ updated: [T] publicKey: String,
change: (UnsafeMutablePointer<config_object>?) throws -> ()
) throws { ) throws {
// Note: This logic assumes that the 'pinnedPriority' values get set correctly elsewhere // Since we are doing direct memory manipulation we are using an `Atomic`
// rather than trying to enforce uniqueness in here (this means if we eventually allow for // type which has blocking access in it's `mutate` closure
// "priority grouping" this logic wouldn't change - just where the priorities get updated let needsPush: Bool = try SessionUtil
// in the HomeVC .config(
for: variant,
publicKey: publicKey
)
.mutate { conf in
guard conf != nil else { throw SessionUtilError.nilConfigObject }
// Peform the change
try change(conf)
// If we don't need to dump the data the we can finish early
guard config_needs_dump(conf) else { return config_needs_push(conf) }
try SessionUtil.createDump(
conf: conf,
for: variant,
publicKey: publicKey
)?.save(db)
return config_needs_push(conf)
}
// Make sure we need a push before scheduling one
guard needsPush else { return }
db.afterNextTransactionNestedOnce(dedupeId: SessionUtil.syncDedupeId(publicKey)) { db in
ConfigurationSyncJob.enqueue(db, publicKey: publicKey)
}
}
@discardableResult static func updatingThreads<T>(_ db: Database, _ updated: [T]) throws -> [T] {
guard let updatedThreads: [SessionThread] = updated as? [SessionThread] else {
throw StorageError.generic
}
// If we have no updated threads then no need to continue
guard !updatedThreads.isEmpty else { return updated }
let userPublicKey: String = getUserHexEncodedPublicKey(db) let userPublicKey: String = getUserHexEncodedPublicKey(db)
let pinnedThreadInfo: [PriorityInfo] = try SessionThread let groupedThreads: [SessionThread.Variant: [SessionThread]] = updatedThreads
.select(.id, .variant, .pinnedPriority)
.asRequest(of: PriorityInfo.self)
.fetchAll(db)
let groupedPriorityInfo: [SessionThread.Variant: [PriorityInfo]] = pinnedThreadInfo
.grouped(by: \.variant) .grouped(by: \.variant)
let pinnedCommunities: [String: OpenGroupUrlInfo] = try OpenGroupUrlInfo let urlInfo: [String: OpenGroupUrlInfo] = try OpenGroupUrlInfo
.fetchAll(db, ids: pinnedThreadInfo.map { $0.id }) .fetchAll(db, ids: updatedThreads.map { $0.id })
.reduce(into: [:]) { result, next in result[next.threadId] = next } .reduce(into: [:]) { result, next in result[next.threadId] = next }
do { do {
try groupedPriorityInfo.forEach { variant, priorityInfo in // Update the unread state for the threads first (just in case that's what changed)
try SessionUtil.updateMarkedAsUnreadState(db, threads: updatedThreads)
// Then update the `hidden` and `priority` values
try groupedThreads.forEach { variant, threads in
switch variant { switch variant {
case .contact: case .contact:
// If the 'Note to Self' conversation is pinned then we need to custom handle it // If the 'Note to Self' conversation is pinned then we need to custom handle it
// first as it's part of the UserProfile config // first as it's part of the UserProfile config
if let noteToSelfPriority: PriorityInfo = priorityInfo.first(where: { $0.id == userPublicKey }) { if let noteToSelf: SessionThread = threads.first(where: { $0.id == userPublicKey }) {
let atomicConf: Atomic<UnsafeMutablePointer<config_object>?> = SessionUtil.config( try SessionUtil.performAndPushChange(
db,
for: .userProfile, for: .userProfile,
publicKey: userPublicKey publicKey: userPublicKey
) ) { conf in
try SessionUtil.updateNoteToSelf(
try SessionUtil.updateNoteToSelfPriority( db,
db, priority: noteToSelf.pinnedPriority
priority: Int32(noteToSelfPriority.pinnedPriority ?? 0), .map { Int32($0 == 0 ? 0 : max($0, 1)) }
in: atomicConf .defaulting(to: 0),
) hidden: noteToSelf.shouldBeVisible,
in: conf
)
}
} }
// Remove the 'Note to Self' convo from the list for updating contact priorities // Remove the 'Note to Self' convo from the list for updating contact priorities
let targetPriorities: [PriorityInfo] = priorityInfo.filter { $0.id != userPublicKey } let remainingThreads: [SessionThread] = threads.filter { $0.id != userPublicKey }
guard !targetPriorities.isEmpty else { return } guard !remainingThreads.isEmpty else { return }
// Since we are doing direct memory manipulation we are using an `Atomic` try SessionUtil.performAndPushChange(
// type which has blocking access in it's `mutate` closure db,
try SessionUtil for: .contacts,
.config( publicKey: userPublicKey
for: .contacts, ) { conf in
publicKey: userPublicKey try SessionUtil.upsert(
contactData: remainingThreads
.map { thread in
SyncedContactInfo(
id: thread.id,
priority: thread.pinnedPriority
.map { Int32($0 == 0 ? 0 : max($0, 1)) }
.defaulting(to: 0),
hidden: thread.shouldBeVisible
)
},
in: conf
) )
.mutate { conf in }
let result: ConfResult = try SessionUtil.upsert(
contactData: targetPriorities
.map { ($0.id, nil, nil, Int32($0.pinnedPriority ?? 0), nil) },
in: conf
)
// If we don't need to dump the data the we can finish early
guard result.needsDump else { return }
try SessionUtil.createDump(
conf: conf,
for: .contacts,
publicKey: userPublicKey
)?.save(db)
}
case .community: case .community:
// Since we are doing direct memory manipulation we are using an `Atomic` try SessionUtil.performAndPushChange(
// type which has blocking access in it's `mutate` closure db,
try SessionUtil for: .userGroups,
.config( publicKey: userPublicKey
for: .userGroups, ) { conf in
publicKey: userPublicKey try SessionUtil.upsert(
communities: threads
.compactMap { thread -> CommunityInfo? in
urlInfo[thread.id].map { urlInfo in
CommunityInfo(
urlInfo: urlInfo,
priority: thread.pinnedPriority
.map { Int32($0 == 0 ? 0 : max($0, 1)) }
.defaulting(to: 0)
)
}
},
in: conf
) )
.mutate { conf in }
let result: ConfResult = try SessionUtil.upsert(
communities: priorityInfo
.compactMap { info in
guard let communityInfo: OpenGroupUrlInfo = pinnedCommunities[info.id] else {
return nil
}
return (communityInfo, info.pinnedPriority)
},
in: conf
)
// If we don't need to dump the data the we can finish early
guard result.needsDump else { return }
try SessionUtil.createDump(
conf: conf,
for: .userGroups,
publicKey: userPublicKey
)?.save(db)
}
case .legacyGroup: case .legacyGroup:
// Since we are doing direct memory manipulation we are using an `Atomic` try SessionUtil.performAndPushChange(
// type which has blocking access in it's `mutate` closure db,
try SessionUtil for: .userGroups,
.config( publicKey: userPublicKey
for: .userGroups, ) { conf in
publicKey: userPublicKey try SessionUtil.upsert(
legacyGroups: threads
.map { thread in
LegacyGroupInfo(
id: thread.id,
hidden: thread.shouldBeVisible,
priority: thread.pinnedPriority
.map { Int32($0 == 0 ? 0 : max($0, 1)) }
.defaulting(to: 0)
)
},
in: conf
) )
.mutate { conf in }
let result: ConfResult = try SessionUtil.upsert(
legacyGroups: priorityInfo
.map { LegacyGroupInfo(id: $0.id, priority: $0.pinnedPriority) },
in: conf
)
// If we don't need to dump the data the we can finish early
guard result.needsDump else { return }
try SessionUtil.createDump(
conf: conf,
for: .userGroups,
publicKey: userPublicKey
)?.save(db)
}
case .group: case .group:
// TODO: Add this // TODO: Add this
@ -155,6 +184,8 @@ internal extension SessionUtil {
catch { catch {
SNLog("[libSession-util] Failed to dump updated data") SNLog("[libSession-util] Failed to dump updated data")
} }
return updated
} }
} }
@ -184,12 +215,13 @@ internal extension SessionUtil {
} }
} }
// MARK: - Pinned Priority // MARK: - PriorityVisibilityInfo
extension SessionUtil { extension SessionUtil {
struct PriorityInfo: Codable, FetchableRecord, Identifiable { struct PriorityVisibilityInfo: Codable, FetchableRecord, Identifiable {
let id: String let id: String
let variant: SessionThread.Variant let variant: SessionThread.Variant
let pinnedPriority: Int32? let pinnedPriority: Int32?
let shouldBeVisible: Bool
} }
} }

View File

@ -8,8 +8,12 @@ import SessionUtilitiesKit
import SessionSnodeKit import SessionSnodeKit
// TODO: Expose 'GROUP_NAME_MAX_LENGTH', 'COMMUNITY_URL_MAX_LENGTH' & 'COMMUNITY_ROOM_MAX_LENGTH' // TODO: Expose 'GROUP_NAME_MAX_LENGTH', 'COMMUNITY_URL_MAX_LENGTH' & 'COMMUNITY_ROOM_MAX_LENGTH'
internal extension SessionUtil { internal extension SessionUtil {
static let columnsRelatedToUserGroups: [ColumnExpression] = [
ClosedGroup.Columns.name
]
// MARK: - Incoming Changes // MARK: - Incoming Changes
static func handleGroupsUpdate( static func handleGroupsUpdate(
_ db: Database, _ db: Database,
in conf: UnsafeMutablePointer<config_object>?, in conf: UnsafeMutablePointer<config_object>?,
@ -20,12 +24,12 @@ internal extension SessionUtil {
guard conf != nil else { throw SessionUtilError.nilConfigObject } guard conf != nil else { throw SessionUtilError.nilConfigObject }
var communities: [PrioritisedData<OpenGroupUrlInfo>] = [] var communities: [PrioritisedData<OpenGroupUrlInfo>] = []
var legacyGroups: [PrioritisedData<LegacyGroupInfo>] = [] var legacyGroups: [LegacyGroupInfo] = []
var groups: [PrioritisedData<String>] = [] var groups: [PrioritisedData<String>] = []
var community: ugroups_community_info = ugroups_community_info() var community: ugroups_community_info = ugroups_community_info()
var legacyGroup: ugroups_legacy_group_info = ugroups_legacy_group_info() var legacyGroup: ugroups_legacy_group_info = ugroups_legacy_group_info()
let groupsIterator: OpaquePointer = user_groups_iterator_new(conf) let groupsIterator: OpaquePointer = user_groups_iterator_new(conf)
while !user_groups_iterator_done(groupsIterator) { while !user_groups_iterator_done(groupsIterator) {
if user_groups_it_is_community(groupsIterator, &community) { if user_groups_it_is_community(groupsIterator, &community) {
let server: String = String(libSessionVal: community.base_url) let server: String = String(libSessionVal: community.base_url)
@ -48,36 +52,52 @@ internal extension SessionUtil {
} }
else if user_groups_it_is_legacy_group(groupsIterator, &legacyGroup) { else if user_groups_it_is_legacy_group(groupsIterator, &legacyGroup) {
let groupId: String = String(libSessionVal: legacyGroup.session_id) 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
}
legacyGroups.append( legacyGroups.append(
PrioritisedData( LegacyGroupInfo(
data: LegacyGroupInfo( id: groupId,
id: groupId, name: String(libSessionVal: legacyGroup.name),
name: String(libSessionVal: legacyGroup.name), lastKeyPair: ClosedGroupKeyPair(
lastKeyPair: ClosedGroupKeyPair( threadId: groupId,
threadId: groupId, publicKey: Data(
publicKey: Data( libSessionVal: legacyGroup.enc_pubkey,
libSessionVal: legacyGroup.enc_pubkey, count: ClosedGroup.pubkeyByteLength
count: ClosedGroup.pubkeyByteLength
),
secretKey: Data(
libSessionVal: legacyGroup.enc_seckey,
count: ClosedGroup.secretKeyByteLength
),
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
), ),
disappearingConfig: DisappearingMessagesConfiguration secretKey: Data(
.defaultWith(groupId) libSessionVal: legacyGroup.enc_seckey,
.with( count: ClosedGroup.secretKeyByteLength
// TODO: double check the 'isEnabled' flag ),
isEnabled: (legacyGroup.disappearing_timer > 0), receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
durationSeconds: (legacyGroup.disappearing_timer == 0 ? nil :
TimeInterval(legacyGroup.disappearing_timer)
)
),
groupMembers: [], //[GroupMember] // TODO: This
hidden: legacyGroup.hidden
), ),
disappearingConfig: DisappearingMessagesConfiguration
.defaultWith(groupId)
.with(
isEnabled: (legacyGroup.disappearing_timer > 0),
durationSeconds: (legacyGroup.disappearing_timer == 0 ? nil :
TimeInterval(legacyGroup.disappearing_timer)
)
),
groupMembers: members.map { memberId, admin in
GroupMember(
groupId: groupId,
profileId: memberId,
role: (admin ? .admin : .standard),
isHidden: false
)
},
hidden: legacyGroup.hidden,
priority: legacyGroup.priority priority: legacyGroup.priority
) )
) )
@ -89,13 +109,13 @@ internal extension SessionUtil {
user_groups_iterator_advance(groupsIterator) user_groups_iterator_advance(groupsIterator)
} }
user_groups_iterator_free(groupsIterator) // Need to free the iterator user_groups_iterator_free(groupsIterator) // Need to free the iterator
// If we don't have any conversations then no need to continue // If we don't have any conversations then no need to continue
guard !communities.isEmpty || !legacyGroups.isEmpty || !groups.isEmpty else { return } guard !communities.isEmpty || !legacyGroups.isEmpty || !groups.isEmpty else { return }
// Extract all community/legacyGroup/group thread priorities // Extract all community/legacyGroup/group thread priorities
let existingThreadPriorities: [String: PriorityInfo] = (try? SessionThread let existingThreadInfo: [String: PriorityVisibilityInfo] = (try? SessionThread
.select(.id, .variant, .pinnedPriority) .select(.id, .variant, .pinnedPriority, .shouldBeVisible)
.filter( .filter(
[ [
SessionThread.Variant.community, SessionThread.Variant.community,
@ -103,7 +123,7 @@ internal extension SessionUtil {
SessionThread.Variant.group SessionThread.Variant.group
].contains(SessionThread.Columns.variant) ].contains(SessionThread.Columns.variant)
) )
.asRequest(of: PriorityInfo.self) .asRequest(of: PriorityVisibilityInfo.self)
.fetchAll(db)) .fetchAll(db))
.defaulting(to: []) .defaulting(to: [])
.reduce(into: [:]) { result, next in result[next.id] = next } .reduce(into: [:]) { result, next in result[next.id] = next }
@ -121,10 +141,10 @@ internal extension SessionUtil {
calledFromConfigHandling: true calledFromConfigHandling: true
) )
.sinkUntilComplete() .sinkUntilComplete()
// Set the priority if it's changed (new communities will have already been inserted at // Set the priority if it's changed (new communities will have already been inserted at
// this stage) // this stage)
if existingThreadPriorities[community.data.threadId]?.pinnedPriority != community.priority { if existingThreadInfo[community.data.threadId]?.pinnedPriority != community.priority {
_ = try? SessionThread _ = try? SessionThread
.filter(id: community.data.threadId) .filter(id: community.data.threadId)
.updateAll( // Handling a config update so don't use `updateAllAndConfig` .updateAll( // Handling a config update so don't use `updateAllAndConfig`
@ -135,7 +155,7 @@ internal extension SessionUtil {
} }
// Remove any communities which are no longer in the config // Remove any communities which are no longer in the config
let communityIdsToRemove: Set<String> = Set(existingThreadPriorities let communityIdsToRemove: Set<String> = Set(existingThreadInfo
.filter { $0.value.variant == .community } .filter { $0.value.variant == .community }
.keys) .keys)
.subtracting(communities.map { $0.data.threadId }) .subtracting(communities.map { $0.data.threadId })
@ -150,22 +170,31 @@ internal extension SessionUtil {
// MARK: -- Handle Legacy Group Changes // MARK: -- Handle Legacy Group Changes
let existingLegacyGroupIds: Set<String> = Set(existingThreadPriorities let existingLegacyGroupIds: Set<String> = Set(existingThreadInfo
.filter { $0.value.variant == .legacyGroup } .filter { $0.value.variant == .legacyGroup }
.keys) .keys)
let existingLegacyGroups: [String: ClosedGroup] = (try? ClosedGroup
.fetchAll(db, ids: existingLegacyGroupIds))
.defaulting(to: [])
.reduce(into: [:]) { result, next in result[next.id] = next }
let existingLegacyGroupMembers: [String: [GroupMember]] = (try? GroupMember
.filter(existingLegacyGroupIds.contains(GroupMember.Columns.groupId))
.fetchAll(db))
.defaulting(to: [])
.grouped(by: \.groupId)
try legacyGroups.forEach { group in try legacyGroups.forEach { group in
guard guard
let name: String = group.data.name, let name: String = group.name,
let lastKeyPair: ClosedGroupKeyPair = group.data.lastKeyPair, let lastKeyPair: ClosedGroupKeyPair = group.lastKeyPair,
let members: [GroupMember] = group.data.groupMembers let members: [GroupMember] = group.groupMembers
else { return } else { return }
if !existingLegacyGroupIds.contains(group.data.id) { if !existingLegacyGroupIds.contains(group.id) {
// Add a new group if it doesn't already exist // Add a new group if it doesn't already exist
try MessageReceiver.handleNewClosedGroup( try MessageReceiver.handleNewClosedGroup(
db, db,
groupPublicKey: group.data.id, groupPublicKey: group.id,
name: name, name: name,
encryptionKeyPair: Box.KeyPair( encryptionKeyPair: Box.KeyPair(
publicKey: lastKeyPair.publicKey.bytes, publicKey: lastKeyPair.publicKey.bytes,
@ -177,20 +206,22 @@ internal extension SessionUtil {
admins: members admins: members
.filter { $0.role == .admin } .filter { $0.role == .admin }
.map { $0.profileId }, .map { $0.profileId },
expirationTimer: UInt32(group.data.disappearingConfig?.durationSeconds ?? 0), expirationTimer: UInt32(group.disappearingConfig?.durationSeconds ?? 0),
messageSentTimestamp: UInt64(latestConfigUpdateSentTimestamp * 1000) messageSentTimestamp: UInt64(latestConfigUpdateSentTimestamp * 1000)
) )
} }
else { else {
// Otherwise update the existing group // Otherwise update the existing group
_ = try? ClosedGroup if existingLegacyGroups[group.id]?.name != name {
.filter(id: group.data.id) _ = try? ClosedGroup
.updateAll( // Handling a config update so don't use `updateAllAndConfig` .filter(id: group.id)
db, .updateAll( // Handling a config update so don't use `updateAllAndConfig`
ClosedGroup.Columns.name.set(to: name) db,
) ClosedGroup.Columns.name.set(to: name)
)
}
// Update the lastKey // Add the lastKey if it doesn't already exist
let keyPairExists: Bool = ClosedGroupKeyPair let keyPairExists: Bool = ClosedGroupKeyPair
.filter( .filter(
ClosedGroupKeyPair.Columns.threadId == lastKeyPair.threadId && ClosedGroupKeyPair.Columns.threadId == lastKeyPair.threadId &&
@ -205,12 +236,11 @@ internal extension SessionUtil {
// Update the disappearing messages timer // Update the disappearing messages timer
_ = try DisappearingMessagesConfiguration _ = try DisappearingMessagesConfiguration
.fetchOne(db, id: group.data.id) .fetchOne(db, id: group.id)
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(group.data.id)) .defaulting(to: DisappearingMessagesConfiguration.defaultWith(group.id))
.with( .with(
// TODO: double check the 'isEnabled' flag isEnabled: (group.disappearingConfig?.isEnabled == true),
isEnabled: (group.data.disappearingConfig?.isEnabled == true), durationSeconds: group.disappearingConfig?.durationSeconds
durationSeconds: group.data.disappearingConfig?.durationSeconds
) )
.saved(db) .saved(db)
@ -221,23 +251,35 @@ internal extension SessionUtil {
// let admins: [String] // let admins: [String]
} }
// TODO: 'hidden' flag - just toggle the 'shouldBeVisible' flag? Delete messages as well??? // Make any thread-specific changes
var threadChanges: [ConfigColumnAssignment] = []
// Set the visibility if it's changed
if existingThreadInfo[group.id]?.shouldBeVisible != (group.hidden == false) {
threadChanges.append(
SessionThread.Columns.shouldBeVisible.set(to: (group.hidden == false))
)
}
// Set the priority if it's changed // Set the priority if it's changed
if existingThreadPriorities[group.data.id]?.pinnedPriority != group.priority { if existingThreadInfo[group.id]?.pinnedPriority != group.priority {
threadChanges.append(
SessionThread.Columns.pinnedPriority.set(to: group.priority)
)
}
if !threadChanges.isEmpty {
_ = try? SessionThread _ = try? SessionThread
.filter(id: group.data.id) .filter(id: group.id)
.updateAll( // Handling a config update so don't use `updateAllAndConfig` .updateAll( // Handling a config update so don't use `updateAllAndConfig`
db, db,
SessionThread.Columns.pinnedPriority.set(to: group.priority) threadChanges
) )
} }
} }
// Remove any legacy groups which are no longer in the config // Remove any legacy groups which are no longer in the config
let legacyGroupIdsToRemove: Set<String> = existingLegacyGroupIds let legacyGroupIdsToRemove: Set<String> = existingLegacyGroupIds
.subtracting(legacyGroups.map { $0.data.id }) .subtracting(legacyGroups.map { $0.id })
if !legacyGroupIdsToRemove.isEmpty { if !legacyGroupIdsToRemove.isEmpty {
try ClosedGroup.removeKeysAndUnsubscribe( try ClosedGroup.removeKeysAndUnsubscribe(
@ -251,16 +293,16 @@ internal extension SessionUtil {
// MARK: -- Handle Group Changes // MARK: -- Handle Group Changes
// TODO: Add this // TODO: Add this
} }
// MARK: - Outgoing Changes // MARK: - Outgoing Changes
static func upsert( static func upsert(
legacyGroups: [LegacyGroupInfo], legacyGroups: [LegacyGroupInfo],
in conf: UnsafeMutablePointer<config_object>? in conf: UnsafeMutablePointer<config_object>?
) throws -> ConfResult { ) throws {
guard conf != nil else { throw SessionUtilError.nilConfigObject } guard conf != nil else { throw SessionUtilError.nilConfigObject }
guard !legacyGroups.isEmpty else { return ConfResult(needsPush: false, needsDump: false) } guard !legacyGroups.isEmpty else { return }
// Since we are doing direct memory manipulation we are using an `Atomic` type which has // Since we are doing direct memory manipulation we are using an `Atomic` type which has
// blocking access in it's `mutate` closure // blocking access in it's `mutate` closure
legacyGroups legacyGroups
@ -268,13 +310,12 @@ internal extension SessionUtil {
var cGroupId: [CChar] = legacyGroup.id.cArray var cGroupId: [CChar] = legacyGroup.id.cArray
let userGroup: UnsafeMutablePointer<ugroups_legacy_group_info> = user_groups_get_or_construct_legacy_group(conf, &cGroupId) let userGroup: UnsafeMutablePointer<ugroups_legacy_group_info> = user_groups_get_or_construct_legacy_group(conf, &cGroupId)
// Assign all properties to match the updated group (if there is one) // Assign all properties to match the updated group (if there is one)
if let updatedName: String = legacyGroup.name { if let updatedName: String = legacyGroup.name {
userGroup.pointee.name = updatedName.toLibSession() userGroup.pointee.name = updatedName.toLibSession()
// Store the updated group (needs to happen before variables go out of scope) // Store the updated group (needs to happen before variables go out of scope)
user_groups_set_legacy_group(conf, &userGroup) user_groups_set_legacy_group(conf, userGroup)
} }
if let lastKeyPair: ClosedGroupKeyPair = legacyGroup.lastKeyPair { if let lastKeyPair: ClosedGroupKeyPair = legacyGroup.lastKeyPair {
@ -282,7 +323,7 @@ internal extension SessionUtil {
userGroup.pointee.enc_seckey = lastKeyPair.secretKey.toLibSession() userGroup.pointee.enc_seckey = lastKeyPair.secretKey.toLibSession()
// Store the updated group (needs to happen before variables go out of scope) // Store the updated group (needs to happen before variables go out of scope)
user_groups_set_legacy_group(conf, &userGroup) user_groups_set_legacy_group(conf, userGroup)
} }
// Assign all properties to match the updated disappearing messages config (if there is one) // Assign all properties to match the updated disappearing messages config (if there is one)
@ -291,10 +332,21 @@ internal extension SessionUtil {
userGroup.pointee.disappearing_timer = (!updatedConfig.isEnabled ? 0 : userGroup.pointee.disappearing_timer = (!updatedConfig.isEnabled ? 0 :
Int64(floor(updatedConfig.durationSeconds)) Int64(floor(updatedConfig.durationSeconds))
) )
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)
}
legacyGroup.groupAdmins?.forEach { member in
var cProfileId: [CChar] = member.profileId.cArray
ugroups_legacy_member_add(userGroup, &cProfileId, true)
} }
// TODO: Need to add members/admins
// Store the updated group (can't be sure if we made any changes above) // Store the updated group (can't be sure if we made any changes above)
userGroup.pointee.hidden = (legacyGroup.hidden ?? userGroup.pointee.hidden) userGroup.pointee.hidden = (legacyGroup.hidden ?? userGroup.pointee.hidden)
userGroup.pointee.priority = (legacyGroup.priority ?? userGroup.pointee.priority) userGroup.pointee.priority = (legacyGroup.priority ?? userGroup.pointee.priority)
@ -302,25 +354,20 @@ internal extension SessionUtil {
// Note: Need to free the legacy group pointer // Note: Need to free the legacy group pointer
user_groups_set_free_legacy_group(conf, userGroup) user_groups_set_free_legacy_group(conf, userGroup)
} }
return ConfResult(
needsPush: config_needs_push(conf),
needsDump: config_needs_dump(conf)
)
} }
static func upsert( static func upsert(
communities: [(info: OpenGroupUrlInfo, priority: Int32?)], communities: [CommunityInfo],
in conf: UnsafeMutablePointer<config_object>? in conf: UnsafeMutablePointer<config_object>?
) throws -> ConfResult { ) throws {
guard conf != nil else { throw SessionUtilError.nilConfigObject } guard conf != nil else { throw SessionUtilError.nilConfigObject }
guard !communities.isEmpty else { return ConfResult.init(needsPush: false, needsDump: false) } guard !communities.isEmpty else { return }
communities communities
.forEach { info, priority in .forEach { community in
var cBaseUrl: [CChar] = info.server.cArray var cBaseUrl: [CChar] = community.urlInfo.server.cArray
var cRoom: [CChar] = info.roomToken.cArray var cRoom: [CChar] = community.urlInfo.roomToken.cArray
var cPubkey: [UInt8] = Data(hex: info.publicKey).cArray var cPubkey: [UInt8] = Data(hex: community.urlInfo.publicKey).cArray
var userCommunity: ugroups_community_info = ugroups_community_info() var userCommunity: ugroups_community_info = ugroups_community_info()
guard user_groups_get_or_construct_community(conf, &userCommunity, &cBaseUrl, &cRoom, &cPubkey) else { guard user_groups_get_or_construct_community(conf, &userCommunity, &cBaseUrl, &cRoom, &cPubkey) else {
@ -328,15 +375,15 @@ internal extension SessionUtil {
return return
} }
userCommunity.priority = (priority ?? userCommunity.priority) userCommunity.priority = (community.priority ?? userCommunity.priority)
user_groups_set_community(conf, &userCommunity) user_groups_set_community(conf, &userCommunity)
} }
return ConfResult(
needsPush: config_needs_push(conf),
needsDump: config_needs_dump(conf)
)
} }
}
// MARK: - External Outgoing Changes
public extension SessionUtil {
// MARK: -- Communities // MARK: -- Communities
@ -346,91 +393,38 @@ internal extension SessionUtil {
rootToken: String, rootToken: String,
publicKey: String publicKey: String
) throws { ) throws {
let userPublicKey: String = getUserHexEncodedPublicKey(db) try SessionUtil.performAndPushChange(
db,
// Since we are doing direct memory manipulation we are using an `Atomic` type which has for: .userGroups,
// blocking access in it's `mutate` closure publicKey: getUserHexEncodedPublicKey(db)
let needsPush: Bool = try SessionUtil ) { conf in
.config( try SessionUtil.upsert(
for: .userGroups, communities: [
publicKey: userPublicKey CommunityInfo(
) urlInfo: OpenGroupUrlInfo(
.mutate { conf in threadId: OpenGroup.idFor(roomToken: rootToken, server: server),
guard conf != nil else { throw SessionUtilError.nilConfigObject } server: server,
roomToken: rootToken,
let result: ConfResult = try SessionUtil.upsert( publicKey: publicKey
communities: [
(
OpenGroupUrlInfo(
threadId: OpenGroup.idFor(roomToken: rootToken, server: server),
server: server,
roomToken: rootToken,
publicKey: publicKey
),
nil
) )
], )
in: conf ],
) in: conf
)
// If we don't need to dump the data the we can finish early
guard result.needsDump else { return result.needsPush }
try SessionUtil.createDump(
conf: conf,
for: .userGroups,
publicKey: userPublicKey
)?.save(db)
return result.needsPush
}
// Make sure we need a push before scheduling one
guard needsPush else { return }
db.afterNextTransactionNestedOnce(dedupeId: SessionUtil.syncDedupeId(userPublicKey)) { db in
ConfigurationSyncJob.enqueue(db, publicKey: userPublicKey)
} }
} }
static func remove(_ db: Database, server: String, roomToken: String) throws { static func remove(_ db: Database, server: String, roomToken: String) throws {
let userPublicKey: String = getUserHexEncodedPublicKey(db) try SessionUtil.performAndPushChange(
db,
// Since we are doing direct memory manipulation we are using an `Atomic` type which has for: .userGroups,
// blocking access in it's `mutate` closure publicKey: getUserHexEncodedPublicKey(db)
let needsPush: Bool = try SessionUtil ) { conf in
.config( var cBaseUrl: [CChar] = server.cArray
for: .userGroups, var cRoom: [CChar] = roomToken.cArray
publicKey: userPublicKey
) // Don't care if the community doesn't exist
.mutate { conf in user_groups_erase_community(conf, &cBaseUrl, &cRoom)
guard conf != nil else { throw SessionUtilError.nilConfigObject }
var cBaseUrl: [CChar] = server.cArray
var cRoom: [CChar] = roomToken.cArray
// Don't care if the community doesn't exist
user_groups_erase_community(conf, &cBaseUrl, &cRoom)
let needsPush: Bool = config_needs_push(conf)
// If we don't need to dump the data the we can finish early
guard config_needs_dump(conf) else { return needsPush }
try SessionUtil.createDump(
conf: conf,
for: .userGroups,
publicKey: userPublicKey
)?.save(db)
return needsPush
}
// Make sure we need a push before scheduling one
guard needsPush else { return }
db.afterNextTransactionNestedOnce(dedupeId: SessionUtil.syncDedupeId(userPublicKey)) { db in
ConfigurationSyncJob.enqueue(db, publicKey: userPublicKey)
} }
} }
@ -446,116 +440,121 @@ internal extension SessionUtil {
members: Set<String>, members: Set<String>,
admins: Set<String> admins: Set<String>
) throws { ) throws {
let userPublicKey: String = getUserHexEncodedPublicKey(db) try SessionUtil.performAndPushChange(
db,
// Since we are doing direct memory manipulation we are using an `Atomic` type which has for: .userGroups,
// blocking access in it's `mutate` closure publicKey: getUserHexEncodedPublicKey(db)
let needsPush: Bool = try SessionUtil ) { conf in
.config( try SessionUtil.upsert(
for: .userGroups, legacyGroups: [
publicKey: userPublicKey LegacyGroupInfo(
) id: groupPublicKey,
.mutate { conf in name: name,
guard conf != nil else { throw SessionUtilError.nilConfigObject } lastKeyPair: ClosedGroupKeyPair(
threadId: groupPublicKey,
let result: ConfResult = try SessionUtil.upsert( publicKey: latestKeyPairPublicKey,
legacyGroups: [ secretKey: latestKeyPairSecretKey,
LegacyGroupInfo( receivedTimestamp: latestKeyPairReceivedTimestamp
id: groupPublicKey, ),
name: name, groupMembers: members
lastKeyPair: ClosedGroupKeyPair( .map { memberId in
threadId: groupPublicKey, GroupMember(
publicKey: latestKeyPairPublicKey, groupId: groupPublicKey,
secretKey: latestKeyPairSecretKey, profileId: memberId,
receivedTimestamp: latestKeyPairReceivedTimestamp role: .standard,
), isHidden: false
groupMembers: members
.map { memberId in
GroupMember(
groupId: groupPublicKey,
profileId: memberId,
role: .standard,
isHidden: false
)
}
.appending(
contentsOf: admins
.map { memberId in
GroupMember(
groupId: groupPublicKey,
profileId: memberId,
role: .admin,
isHidden: false
)
}
) )
) },
], groupAdmins: admins
in: conf .map { memberId in
) GroupMember(
groupId: groupPublicKey,
// If we don't need to dump the data the we can finish early profileId: memberId,
guard result.needsDump else { return result.needsPush } role: .admin,
isHidden: false
try SessionUtil.createDump( )
conf: conf, }
for: .userGroups, )
publicKey: userPublicKey ],
)?.save(db) in: conf
)
return result.needsPush }
} }
// Make sure we need a push before scheduling one static func update(
guard needsPush else { return } _ db: Database,
groupPublicKey: String,
db.afterNextTransactionNestedOnce(dedupeId: SessionUtil.syncDedupeId(userPublicKey)) { db in name: String? = nil,
ConfigurationSyncJob.enqueue(db, publicKey: userPublicKey) latestKeyPair: ClosedGroupKeyPair? = nil,
members: Set<String>? = nil,
admins: Set<String>? = nil
) throws {
try SessionUtil.performAndPushChange(
db,
for: .userGroups,
publicKey: getUserHexEncodedPublicKey(db)
) { conf in
try SessionUtil.upsert(
legacyGroups: [
LegacyGroupInfo(
id: groupPublicKey,
name: name,
lastKeyPair: latestKeyPair,
groupMembers: members?
.map { memberId in
GroupMember(
groupId: groupPublicKey,
profileId: memberId,
role: .standard,
isHidden: false
)
},
groupAdmins: admins?
.map { memberId in
GroupMember(
groupId: groupPublicKey,
profileId: memberId,
role: .admin,
isHidden: false
)
}
)
],
in: conf
)
} }
} }
static func hide(_ db: Database, legacyGroupIds: [String]) throws { static func hide(_ db: Database, legacyGroupIds: [String]) throws {
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 { static func remove(_ db: Database, legacyGroupIds: [String]) throws {
let userPublicKey: String = getUserHexEncodedPublicKey(db) try SessionUtil.performAndPushChange(
db,
// Since we are doing direct memory manipulation we are using an `Atomic` type which has for: .userGroups,
// blocking access in it's `mutate` closure publicKey: getUserHexEncodedPublicKey(db)
let needsPush: Bool = try SessionUtil ) { conf in
.config( legacyGroupIds.forEach { threadId in
for: .userGroups, var cGroupId: [CChar] = threadId.cArray
publicKey: userPublicKey
)
.mutate { conf in
guard conf != nil else { throw SessionUtilError.nilConfigObject }
legacyGroupIds.forEach { threadId in // Don't care if the group doesn't exist
var cGroupId: [CChar] = threadId.cArray user_groups_erase_legacy_group(conf, &cGroupId)
// Don't care if the group doesn't exist
user_groups_erase_legacy_group(conf, &cGroupId)
}
let needsPush: Bool = config_needs_push(conf)
// If we don't need to dump the data the we can finish early
guard config_needs_dump(conf) else { return needsPush }
try SessionUtil.createDump(
conf: conf,
for: .userGroups,
publicKey: userPublicKey
)?.save(db)
return needsPush
} }
// Make sure we need a push before scheduling one
guard needsPush else { return }
db.afterNextTransactionNestedOnce(dedupeId: SessionUtil.syncDedupeId(userPublicKey)) { db in
ConfigurationSyncJob.enqueue(db, publicKey: userPublicKey)
} }
} }
@ -568,12 +567,6 @@ internal extension SessionUtil {
} }
} }
}
return updated
}
}
// MARK: - LegacyGroupInfo // MARK: - LegacyGroupInfo
extension SessionUtil { extension SessionUtil {
@ -585,6 +578,7 @@ extension SessionUtil {
case lastKeyPair case lastKeyPair
case disappearingConfig case disappearingConfig
case groupMembers case groupMembers
case groupAdmins
case hidden case hidden
case priority case priority
} }
@ -596,6 +590,7 @@ extension SessionUtil {
let lastKeyPair: ClosedGroupKeyPair? let lastKeyPair: ClosedGroupKeyPair?
let disappearingConfig: DisappearingMessagesConfiguration? let disappearingConfig: DisappearingMessagesConfiguration?
let groupMembers: [GroupMember]? let groupMembers: [GroupMember]?
let groupAdmins: [GroupMember]?
let hidden: Bool? let hidden: Bool?
let priority: Int32? let priority: Int32?
@ -605,6 +600,7 @@ extension SessionUtil {
lastKeyPair: ClosedGroupKeyPair? = nil, lastKeyPair: ClosedGroupKeyPair? = nil,
disappearingConfig: DisappearingMessagesConfiguration? = nil, disappearingConfig: DisappearingMessagesConfiguration? = nil,
groupMembers: [GroupMember]? = nil, groupMembers: [GroupMember]? = nil,
groupAdmins: [GroupMember]? = nil,
hidden: Bool? = nil, hidden: Bool? = nil,
priority: Int32? = nil priority: Int32? = nil
) { ) {
@ -613,6 +609,7 @@ extension SessionUtil {
self.lastKeyPair = lastKeyPair self.lastKeyPair = lastKeyPair
self.disappearingConfig = disappearingConfig self.disappearingConfig = disappearingConfig
self.groupMembers = groupMembers self.groupMembers = groupMembers
self.groupAdmins = groupAdmins
self.hidden = hidden self.hidden = hidden
self.priority = priority self.priority = priority
} }
@ -625,7 +622,17 @@ extension SessionUtil {
.order(ClosedGroupKeyPair.Columns.receivedTimestamp.desc) .order(ClosedGroupKeyPair.Columns.receivedTimestamp.desc)
.forKey(Columns.lastKeyPair.name) .forKey(Columns.lastKeyPair.name)
) )
.including(all: ClosedGroup.members) .including(
all: ClosedGroup.members
.filter([GroupMember.Role.standard, GroupMember.Role.zombie]
.contains(GroupMember.Columns.role))
.forKey(Columns.groupMembers.name)
)
.including(
all: ClosedGroup.members
.filter(GroupMember.Columns.role == GroupMember.Role.admin)
.forKey(Columns.groupAdmins.name)
)
.joining( .joining(
optional: ClosedGroup.thread optional: ClosedGroup.thread
.including( .including(
@ -638,6 +645,19 @@ extension SessionUtil {
} }
} }
struct CommunityInfo {
let urlInfo: OpenGroupUrlInfo
let priority: Int32?
init(
urlInfo: OpenGroupUrlInfo,
priority: Int32? = nil
) {
self.urlInfo = urlInfo
self.priority = priority
}
}
fileprivate struct GroupThreadData { fileprivate struct GroupThreadData {
let communities: [PrioritisedData<SessionUtil.OpenGroupUrlInfo>] let communities: [PrioritisedData<SessionUtil.OpenGroupUrlInfo>]
let legacyGroups: [PrioritisedData<LegacyGroupInfo>] let legacyGroups: [PrioritisedData<LegacyGroupInfo>]

View File

@ -76,7 +76,7 @@ internal extension SessionUtil {
static func update( static func update(
profile: Profile, profile: Profile,
in conf: UnsafeMutablePointer<config_object>? in conf: UnsafeMutablePointer<config_object>?
) throws -> ConfResult { ) throws {
guard conf != nil else { throw SessionUtilError.nilConfigObject } guard conf != nil else { throw SessionUtilError.nilConfigObject }
// Update the name // Update the name
@ -88,35 +88,17 @@ internal extension SessionUtil {
profilePic.url = profile.profilePictureUrl.toLibSession() profilePic.url = profile.profilePictureUrl.toLibSession()
profilePic.key = profile.profileEncryptionKey.toLibSession() profilePic.key = profile.profileEncryptionKey.toLibSession()
user_profile_set_pic(conf, profilePic) user_profile_set_pic(conf, profilePic)
return ConfResult(
needsPush: config_needs_push(conf),
needsDump: config_needs_dump(conf)
)
} }
static func updateNoteToSelfPriority( static func updateNoteToSelf(
_ db: Database, _ db: Database,
priority: Int32, priority: Int32,
in atomicConf: Atomic<UnsafeMutablePointer<config_object>?> hidden: Bool,
in conf: UnsafeMutablePointer<config_object>?
) throws { ) throws {
guard atomicConf.wrappedValue != nil else { throw SessionUtilError.nilConfigObject } guard conf != nil else { throw SessionUtilError.nilConfigObject }
let userPublicKey: String = getUserHexEncodedPublicKey(db) user_profile_set_nts_priority(conf, priority)
user_profile_set_nts_hidden(conf, hidden)
// Since we are doing direct memory manipulation we are using an `Atomic` type which has
// blocking access in it's `mutate` closure
try atomicConf.mutate { conf in
user_profile_set_nts_priority(conf, priority)
// If we don't need to dump the data the we can finish early
guard config_needs_dump(conf) else { return }
try SessionUtil.createDump(
conf: conf,
for: .userProfile,
publicKey: userPublicKey
)?.save(db)
}
} }
} }

View File

@ -130,9 +130,11 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table
case is QueryInterfaceRequest<Profile>: case is QueryInterfaceRequest<Profile>:
return try SessionUtil.updatingProfiles(db, updatedData) return try SessionUtil.updatingProfiles(db, updatedData)
case is QueryInterfaceRequest<SessionThread>: case is QueryInterfaceRequest<ClosedGroup>:
return updatedData return updatedData
case is QueryInterfaceRequest<SessionThread>:
return try SessionUtil.updatingThreads(db, updatedData)
default: return updatedData default: return updatedData
} }

View File

@ -4,6 +4,18 @@
<dict> <dict>
<key>AvailableLibraries</key> <key>AvailableLibraries</key>
<array> <array>
<dict>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>libsession-util.a</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
</dict>
<dict> <dict>
<key>LibraryIdentifier</key> <key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string> <string>ios-arm64_x86_64-simulator</string>
@ -19,18 +31,6 @@
<key>SupportedPlatformVariant</key> <key>SupportedPlatformVariant</key>
<string>simulator</string> <string>simulator</string>
</dict> </dict>
<dict>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>libsession-util.a</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
</dict>
</array> </array>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>XFWK</string> <string>XFWK</string>

View File

@ -118,8 +118,8 @@ bool config_needs_dump(const config_object* conf);
/// Struct containing a list of C strings. Typically where this is returned by this API it must be /// Struct containing a list of C strings. Typically where this is returned by this API it must be
/// freed (via `free()`) when done with it. /// freed (via `free()`) when done with it.
typedef struct config_string_list { typedef struct config_string_list {
char** value; // array of null-terminated C strings char** value; // array of null-terminated C strings
size_t len; // length of `value` size_t len; // length of `value`
} config_string_list; } config_string_list;
/// Obtains the current active hashes. Note that this will be empty if the current hash is unknown /// Obtains the current active hashes. Note that this will be empty if the current hash is unknown

View File

@ -31,7 +31,11 @@ bool community_parse_full_url(
// may be NULL in which case it is not set (typically both pubkey arguments would be null for cases // may be NULL in which case it is not set (typically both pubkey arguments would be null for cases
// where you don't care at all about the pubkey). // where you don't care at all about the pubkey).
bool community_parse_partial_url( bool community_parse_partial_url(
const char* full_url, char* base_url, char* room_token, unsigned char* pubkey, bool* has_pubkey); const char* full_url,
char* base_url,
char* room_token,
unsigned char* pubkey,
bool* has_pubkey);
// Produces a standard full URL from a given base_url (c string), room token (c string), and pubkey // Produces a standard full URL from a given base_url (c string), room token (c string), and pubkey
// (fixed-length 32 byte buffer). The full URL is written to `full_url`, which must be at least // (fixed-length 32 byte buffer). The full URL is written to `full_url`, which must be at least

View File

@ -56,6 +56,12 @@ int user_profile_get_nts_priority(const config_object* conf);
// Sets the current note-to-self priority level. Should be >= 0 (negatives will be set to 0). // Sets the current note-to-self priority level. Should be >= 0 (negatives will be set to 0).
void user_profile_set_nts_priority(config_object* conf, int priority); void user_profile_set_nts_priority(config_object* conf, int priority);
// Gets the current note-to-self priority level. Will always be >= 0.
bool user_profile_get_nts_hidden(const config_object* conf);
// Sets the current note-to-self priority level. Should be >= 0 (negatives will be set to 0).
void user_profile_set_nts_hidden(config_object* conf, bool hidden);
#ifdef __cplusplus #ifdef __cplusplus
} // extern "C" } // extern "C"
#endif #endif

View File

@ -16,6 +16,8 @@ namespace session::config {
/// q - user profile decryption key (binary) /// q - user profile decryption key (binary)
/// + - the priority value for the "Note to Self" pseudo-conversation (higher = higher in the /// + - the priority value for the "Note to Self" pseudo-conversation (higher = higher in the
/// conversation list). Omitted when 0. /// conversation list). Omitted when 0.
/// h - the "hidden" value for the "Note to Self" pseudo-conversation (true = hide). Omitted when
/// false.
class UserProfile final : public ConfigBase { class UserProfile final : public ConfigBase {
@ -55,11 +57,18 @@ class UserProfile final : public ConfigBase {
void set_profile_pic(std::string_view url, ustring_view key); void set_profile_pic(std::string_view url, ustring_view key);
void set_profile_pic(profile_pic pic); void set_profile_pic(profile_pic pic);
/// Gets/sets the Note-to-self conversation priority. Will always be >= 0. /// Gets the Note-to-self conversation priority. Will always be >= 0.
int get_nts_priority() const; int get_nts_priority() const;
/// Sets the Note-to-self conversation priority. Should be >= 0 (negatives will be set to 0). /// Sets the Note-to-self conversation priority. Should be >= 0 (negatives will be set to 0).
void set_nts_priority(int priority); void set_nts_priority(int priority);
/// Gets the Note-to-self hidden flag; true means the Note-to-self "conversation" should be
/// hidden from the conversation list.
bool get_nts_hidden() const;
/// Sets or clears the `hidden` flag that hides the Note-to-self from the conversation list.
void set_nts_hidden(bool hidden);
}; };
} // namespace session::config } // namespace session::config

View File

@ -8,12 +8,10 @@ import SessionUtilitiesKit
/// Abstract base class for `VisibleMessage` and `ControlMessage`. /// Abstract base class for `VisibleMessage` and `ControlMessage`.
public class Message: Codable { public class Message: Codable {
public var id: String? public var id: String?
public var threadId: String?
public var sentTimestamp: UInt64? public var sentTimestamp: UInt64?
public var receivedTimestamp: UInt64? public var receivedTimestamp: UInt64?
public var recipient: String? public var recipient: String?
public var sender: String? public var sender: String?
public var groupPublicKey: String?
public var openGroupServerMessageId: UInt64? public var openGroupServerMessageId: UInt64?
public var serverHash: String? public var serverHash: String?
@ -34,7 +32,6 @@ public class Message: Codable {
public init( public init(
id: String? = nil, id: String? = nil,
threadId: String? = nil,
sentTimestamp: UInt64? = nil, sentTimestamp: UInt64? = nil,
receivedTimestamp: UInt64? = nil, receivedTimestamp: UInt64? = nil,
recipient: String? = nil, recipient: String? = nil,
@ -44,12 +41,10 @@ public class Message: Codable {
serverHash: String? = nil serverHash: String? = nil
) { ) {
self.id = id self.id = id
self.threadId = threadId
self.sentTimestamp = sentTimestamp self.sentTimestamp = sentTimestamp
self.receivedTimestamp = receivedTimestamp self.receivedTimestamp = receivedTimestamp
self.recipient = recipient self.recipient = recipient
self.sender = sender self.sender = sender
self.groupPublicKey = groupPublicKey
self.openGroupServerMessageId = openGroupServerMessageId self.openGroupServerMessageId = openGroupServerMessageId
self.serverHash = serverHash self.serverHash = serverHash
} }
@ -68,14 +63,13 @@ public class Message: Codable {
// MARK: - Message Parsing/Processing // MARK: - Message Parsing/Processing
public typealias ProcessedMessage = ( public typealias ProcessedMessage = (
threadId: String?, threadId: String,
threadVariant: SessionThread.Variant,
proto: SNProtoContent, proto: SNProtoContent,
messageInfo: MessageReceiveJob.Details.MessageInfo messageInfo: MessageReceiveJob.Details.MessageInfo
) )
public extension Message { public extension Message {
static let nonThreadMessageId: String = "NON_THREAD_MESSAGE"
enum Variant: String, Codable { enum Variant: String, Codable {
case readReceipt case readReceipt
case typingIndicator case typingIndicator
@ -485,7 +479,7 @@ public extension Message {
handleClosedGroupKeyUpdateMessages: Bool, handleClosedGroupKeyUpdateMessages: Bool,
dependencies: SMKDependencies = SMKDependencies() dependencies: SMKDependencies = SMKDependencies()
) throws -> ProcessedMessage? { ) throws -> ProcessedMessage? {
let (message, proto, threadId) = try MessageReceiver.parse( let (message, proto, threadId, threadVariant) = try MessageReceiver.parse(
db, db,
envelope: envelope, envelope: envelope,
serverExpirationTimestamp: serverExpirationTimestamp, serverExpirationTimestamp: serverExpirationTimestamp,
@ -511,7 +505,12 @@ public extension Message {
case let closedGroupControlMessage as ClosedGroupControlMessage: case let closedGroupControlMessage as ClosedGroupControlMessage:
switch closedGroupControlMessage.kind { switch closedGroupControlMessage.kind {
case .encryptionKeyPair: case .encryptionKeyPair:
try MessageReceiver.handleClosedGroupControlMessage(db, closedGroupControlMessage) try MessageReceiver.handleClosedGroupControlMessage(
db,
threadId: threadId,
threadVariant: threadVariant,
message: closedGroupControlMessage
)
return nil return nil
default: break default: break
@ -540,10 +539,12 @@ public extension Message {
return ( return (
threadId, threadId,
threadVariant,
proto, proto,
try MessageReceiveJob.Details.MessageInfo( try MessageReceiveJob.Details.MessageInfo(
message: message, message: message,
variant: variant, variant: variant,
threadVariant: threadVariant,
serverExpirationTimestamp: serverExpirationTimestamp, serverExpirationTimestamp: serverExpirationTimestamp,
proto: proto proto: proto
) )

View File

@ -229,17 +229,19 @@ public final class OpenGroupManager {
let threadId: String = OpenGroup.idFor(roomToken: roomToken, server: targetServer) let threadId: String = OpenGroup.idFor(roomToken: roomToken, server: targetServer)
// Optionally try to insert a new version of the OpenGroup (it will fail if there is already an // Optionally try to insert a new version of the OpenGroup (it will fail if there is already an
// inactive one but that won't matter as we then activate it // inactive one but that won't matter as we then activate it)
_ = try? SessionThread.fetchOrCreate(db, id: threadId, variant: .community) _ = try? SessionThread
.fetchOrCreate(
// If we didn't add this open group via config handling then flag it to be visible (if it did db,
// come via config handling then we want to wait until it actually has messages before making id: threadId,
// it visible) variant: .community,
if !calledFromConfigHandling { /// If we didn't add this open group via config handling then flag it to be visible (if it did come via config handling then
_ = try? SessionThread /// we want to wait until it actually has messages before making it visible)
.filter(id: threadId) ///
.updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true)) /// **Note:** We **MUST** provide a `nil` value if this method was called from the config handling as updating
} /// the `shouldVeVisible` state can trigger a config update which could result in an infinite loop in the future
shouldBeVisible: (calledFromConfigHandling ? nil : true)
)
if (try? OpenGroup.exists(db, id: threadId)) == false { if (try? OpenGroup.exists(db, id: threadId)) == false {
try? OpenGroup try? OpenGroup
@ -641,10 +643,11 @@ public final class OpenGroupManager {
if let messageInfo: MessageReceiveJob.Details.MessageInfo = processedMessage?.messageInfo { if let messageInfo: MessageReceiveJob.Details.MessageInfo = processedMessage?.messageInfo {
try MessageReceiver.handle( try MessageReceiver.handle(
db, db,
threadId: openGroup.id,
threadVariant: .community,
message: messageInfo.message, message: messageInfo.message,
serverExpirationTimestamp: messageInfo.serverExpirationTimestamp, serverExpirationTimestamp: messageInfo.serverExpirationTimestamp,
associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData), associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData),
openGroupId: openGroup.id,
dependencies: dependencies dependencies: dependencies
) )
} }
@ -805,10 +808,11 @@ public final class OpenGroupManager {
if let messageInfo: MessageReceiveJob.Details.MessageInfo = processedMessage?.messageInfo { if let messageInfo: MessageReceiveJob.Details.MessageInfo = processedMessage?.messageInfo {
try MessageReceiver.handle( try MessageReceiver.handle(
db, db,
threadId: (lookup.sessionId ?? lookup.blindedId),
threadVariant: .contact, // Technically not open group messages
message: messageInfo.message, message: messageInfo.message,
serverExpirationTimestamp: messageInfo.serverExpirationTimestamp, serverExpirationTimestamp: messageInfo.serverExpirationTimestamp,
associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData), associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData),
openGroupId: nil, // Intentionally nil as they are technically not open group messages
dependencies: dependencies dependencies: dependencies
) )
} }

View File

@ -7,7 +7,15 @@ import SessionUtilitiesKit
import SessionSnodeKit import SessionSnodeKit
extension MessageReceiver { extension MessageReceiver {
public static func handleCallMessage(_ db: Database, message: CallMessage) throws { public static func handleCallMessage(
_ db: Database,
threadId: String,
threadVariant: SessionThread.Variant,
message: CallMessage
) throws {
// Only support calls from contact threads
guard threadVariant == .contact else { return }
switch message.kind { switch message.kind {
case .preOffer: try MessageReceiver.handleNewCallMessage(db, message: message) case .preOffer: try MessageReceiver.handleNewCallMessage(db, message: message)
case .offer: MessageReceiver.handleOfferCallMessage(db, message: message) case .offer: MessageReceiver.handleOfferCallMessage(db, message: message)
@ -43,12 +51,18 @@ extension MessageReceiver {
guard guard
CurrentAppContext().isMainApp, CurrentAppContext().isMainApp,
let sender: String = message.sender, let sender: String = message.sender,
(try? Contact.fetchOne(db, id: sender))?.isApproved == true (try? Contact
.filter(id: sender)
.select(.isApproved)
.asRequest(of: Bool.self)
.fetchOne(db))
.defaulting(to: false)
else { return } else { return }
guard let timestamp = message.sentTimestamp, TimestampUtils.isWithinOneMinute(timestamp: timestamp) else { guard let timestamp = message.sentTimestamp, TimestampUtils.isWithinOneMinute(timestamp: timestamp) else {
// Add missed call message for call offer messages from more than one minute // Add missed call message for call offer messages from more than one minute
if let interaction: Interaction = try MessageReceiver.insertCallInfoMessage(db, for: message, state: .missed) { if let interaction: Interaction = try MessageReceiver.insertCallInfoMessage(db, for: message, state: .missed) {
let thread: SessionThread = try SessionThread.fetchOrCreate(db, id: sender, variant: .contact) let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: sender, variant: .contact, shouldBeVisible: nil)
Environment.shared?.notificationsManager.wrappedValue? Environment.shared?.notificationsManager.wrappedValue?
.notifyUser( .notifyUser(
@ -62,7 +76,8 @@ extension MessageReceiver {
guard db[.areCallsEnabled] else { guard db[.areCallsEnabled] else {
if let interaction: Interaction = try MessageReceiver.insertCallInfoMessage(db, for: message, state: .permissionDenied) { if let interaction: Interaction = try MessageReceiver.insertCallInfoMessage(db, for: message, state: .permissionDenied) {
let thread: SessionThread = try SessionThread.fetchOrCreate(db, id: sender, variant: .contact) let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: sender, variant: .contact, shouldBeVisible: nil)
Environment.shared?.notificationsManager.wrappedValue? Environment.shared?.notificationsManager.wrappedValue?
.notifyUser( .notifyUser(

View File

@ -68,7 +68,6 @@ extension MessageReceiver {
guard hasApprovedAdmin else { return } guard hasApprovedAdmin else { return }
// Create the group // Create the group
let groupAlreadyExisted: Bool = ((try? SessionThread.exists(db, id: groupPublicKey)) ?? false)
let thread: SessionThread = try SessionThread let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup) .fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup)
.with(shouldBeVisible: true) .with(shouldBeVisible: true)
@ -84,39 +83,23 @@ extension MessageReceiver {
try closedGroup.zombies.deleteAll(db) try closedGroup.zombies.deleteAll(db)
} }
// Notify the user // Create the GroupMember records if needed
if !groupAlreadyExisted { try members.forEach { memberId in
// Create the GroupMember records try GroupMember(
try members.forEach { memberId in groupId: groupPublicKey,
try GroupMember( profileId: memberId,
groupId: groupPublicKey, role: .standard,
profileId: memberId, isHidden: false
role: .standard, ).save(db)
isHidden: false }
).save(db)
} try admins.forEach { adminId in
try GroupMember(
try admins.forEach { adminId in groupId: groupPublicKey,
try GroupMember( profileId: adminId,
groupId: groupPublicKey, role: .admin,
profileId: adminId, isHidden: false
role: .admin, ).save(db)
isHidden: false
).save(db)
}
// Note: We don't provide a `serverHash` in this case as we want to allow duplicates
// to avoid the following situation:
// The app performed a background poll or received a push notification
// This method was invoked and the received message timestamps table was updated
// Processing wasn't finished
// The user doesn't see the new closed group
_ = try Interaction(
threadId: thread.id,
authorId: getUserHexEncodedPublicKey(db),
variant: .infoClosedGroupCreated,
timestampMs: Int64(messageSentTimestamp)
).inserted(db)
} }
// Update the DisappearingMessages config // Update the DisappearingMessages config
@ -194,12 +177,20 @@ extension MessageReceiver {
} }
do { do {
try ClosedGroupKeyPair( let keyPair: ClosedGroupKeyPair = ClosedGroupKeyPair(
threadId: groupPublicKey, threadId: groupPublicKey,
publicKey: proto.publicKey.removingIdPrefixIfNeeded(), publicKey: proto.publicKey.removingIdPrefixIfNeeded(),
secretKey: proto.privateKey, secretKey: proto.privateKey,
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
).insert(db) )
try keyPair.insert(db)
// Update libSession
try? SessionUtil.update(
db,
groupPublicKey: groupPublicKey,
latestKeyPair: keyPair
)
} }
catch { catch {
if case DatabaseError.SQLITE_CONSTRAINT_UNIQUE = error { if case DatabaseError.SQLITE_CONSTRAINT_UNIQUE = error {
@ -236,6 +227,13 @@ extension MessageReceiver {
SnodeAPI.currentOffsetTimestampMs() SnodeAPI.currentOffsetTimestampMs()
) )
).inserted(db) ).inserted(db)
// Update libSession
try? SessionUtil.update(
db,
groupPublicKey: id,
name: name
)
} }
} }
@ -243,12 +241,16 @@ extension MessageReceiver {
guard case let .membersAdded(membersAsData) = message.kind else { return } guard case let .membersAdded(membersAsData) = message.kind else { return }
try performIfValid(db, message: message) { id, sender, thread, closedGroup in try performIfValid(db, message: message) { id, sender, thread, closedGroup in
guard let groupMembers: [GroupMember] = try? closedGroup.members.fetchAll(db) else { return } guard let allGroupMembers: [GroupMember] = try? closedGroup.allMembers.fetchAll(db) else {
guard let groupAdmins: [GroupMember] = try? closedGroup.admins.fetchAll(db) else { return } return
}
// Update the group // Update the group
let addedMembers: [String] = membersAsData.map { $0.toHexString() } let addedMembers: [String] = membersAsData.map { $0.toHexString() }
let currentMemberIds: Set<String> = groupMembers.map { $0.profileId }.asSet() let currentMemberIds: Set<String> = allGroupMembers
.filter { $0.role == .standard }
.map { $0.profileId }
.asSet()
let members: Set<String> = currentMemberIds.union(addedMembers) let members: Set<String> = currentMemberIds.union(addedMembers)
// Create records for any new members // Create records for any new members
@ -278,7 +280,7 @@ extension MessageReceiver {
// generated by the admin when they saw the member removed message. // generated by the admin when they saw the member removed message.
let userPublicKey: String = getUserHexEncodedPublicKey(db) let userPublicKey: String = getUserHexEncodedPublicKey(db)
if groupAdmins.contains(where: { $0.profileId == userPublicKey }) { if allGroupMembers.contains(where: { $0.role == .admin && $0.profileId == userPublicKey }) {
addedMembers.forEach { memberId in addedMembers.forEach { memberId in
MessageSender.sendLatestEncryptionKeyPair(db, to: memberId, for: id) MessageSender.sendLatestEncryptionKeyPair(db, to: memberId, for: id)
} }
@ -292,7 +294,7 @@ extension MessageReceiver {
.deleteAll(db) .deleteAll(db)
// Notify the user if needed // Notify the user if needed
guard members != Set(groupMembers.map { $0.profileId }) else { return } guard members != currentMemberIds else { return }
_ = try Interaction( _ = try Interaction(
serverHash: message.serverHash, serverHash: message.serverHash,
@ -303,7 +305,7 @@ extension MessageReceiver {
.membersAdded( .membersAdded(
members: addedMembers members: addedMembers
.asSet() .asSet()
.subtracting(groupMembers.map { $0.profileId }) .subtracting(currentMemberIds)
.map { Data(hex: $0) } .map { Data(hex: $0) }
) )
.infoMessage(db, sender: sender), .infoMessage(db, sender: sender),
@ -312,6 +314,21 @@ extension MessageReceiver {
SnodeAPI.currentOffsetTimestampMs() SnodeAPI.currentOffsetTimestampMs()
) )
).inserted(db) ).inserted(db)
// Update libSession
try? SessionUtil.update(
db,
groupPublicKey: id,
members: allGroupMembers
.filter { $0.role == .standard || $0.role == .zombie }
.map { $0.profileId }
.asSet()
.union(addedMembers),
admins: allGroupMembers
.filter { $0.role == .admin }
.map { $0.profileId }
.asSet()
)
} }
} }
@ -325,17 +342,22 @@ extension MessageReceiver {
try performIfValid(db, message: message) { id, sender, thread, closedGroup in try performIfValid(db, message: message) { id, sender, thread, closedGroup in
// Check that the admin wasn't removed // Check that the admin wasn't removed
guard let groupMembers: [GroupMember] = try? closedGroup.members.fetchAll(db) else { return } guard let allGroupMembers: [GroupMember] = try? closedGroup.allMembers.fetchAll(db) else {
guard let groupAdmins: [GroupMember] = try? closedGroup.admins.fetchAll(db) else { return } return
}
let removedMembers = membersAsData.map { $0.toHexString() } let removedMembers = membersAsData.map { $0.toHexString() }
let members = Set(groupMembers.map { $0.profileId }).subtracting(removedMembers) let currentMemberIds: Set<String> = allGroupMembers
.filter { $0.role == .standard }
.map { $0.profileId }
.asSet()
let members = currentMemberIds.subtracting(removedMembers)
guard let firstAdminId: String = groupAdmins.first?.profileId, members.contains(firstAdminId) else { guard let firstAdminId: String = allGroupMembers.filter({ $0.role == .admin }).first?.profileId, members.contains(firstAdminId) else {
return SNLog("Ignoring invalid closed group update.") return SNLog("Ignoring invalid closed group update.")
} }
// Check that the message was sent by the group admin // Check that the message was sent by the group admin
guard groupAdmins.contains(where: { $0.profileId == sender }) else { guard allGroupMembers.filter({ $0.role == .admin }).contains(where: { $0.profileId == sender }) else {
return SNLog("Ignoring invalid closed group update.") return SNLog("Ignoring invalid closed group update.")
} }
@ -368,7 +390,7 @@ extension MessageReceiver {
} }
// Notify the user if needed // Notify the user if needed
guard members != Set(groupMembers.map { $0.profileId }) else { return } guard members != currentMemberIds else { return }
_ = try Interaction( _ = try Interaction(
serverHash: message.serverHash, serverHash: message.serverHash,
@ -379,7 +401,7 @@ extension MessageReceiver {
.membersRemoved( .membersRemoved(
members: removedMembers members: removedMembers
.asSet() .asSet()
.intersection(groupMembers.map { $0.profileId }) .intersection(currentMemberIds)
.map { Data(hex: $0) } .map { Data(hex: $0) }
) )
.infoMessage(db, sender: sender), .infoMessage(db, sender: sender),
@ -388,6 +410,21 @@ extension MessageReceiver {
SnodeAPI.currentOffsetTimestampMs() SnodeAPI.currentOffsetTimestampMs()
) )
).inserted(db) ).inserted(db)
// Update libSession
try? SessionUtil.update(
db,
groupPublicKey: id,
members: allGroupMembers
.filter { $0.role == .standard || $0.role == .zombie }
.map { $0.profileId }
.asSet()
.subtracting(removedMembers),
admins: allGroupMembers
.filter { $0.role == .admin }
.map { $0.profileId }
.asSet()
)
} }
} }
@ -466,6 +503,23 @@ extension MessageReceiver {
SnodeAPI.currentOffsetTimestampMs() SnodeAPI.currentOffsetTimestampMs()
) )
).inserted(db) ).inserted(db)
// Update libSession
try? SessionUtil.update(
db,
groupPublicKey: id,
members: allGroupMembers
.filter {
($0.role == .standard || $0.role == .zombie) &&
!membersToRemove.contains($0)
}
.map { $0.profileId }
.asSet(),
admins: allGroupMembers
.filter { $0.role == .admin }
.map { $0.profileId }
.asSet()
)
} }
} }

View File

@ -5,17 +5,21 @@ import GRDB
import SessionSnodeKit import SessionSnodeKit
extension MessageReceiver { extension MessageReceiver {
internal static func handleDataExtractionNotification(_ db: Database, message: DataExtractionNotification) throws { internal static func handleDataExtractionNotification(
_ db: Database,
threadId: String,
threadVariant: SessionThread.Variant,
message: DataExtractionNotification
) throws {
guard guard
threadVariant == .contact,
let sender: String = message.sender, let sender: String = message.sender,
let messageKind: DataExtractionNotification.Kind = message.kind, let messageKind: DataExtractionNotification.Kind = message.kind
let thread: SessionThread = try? SessionThread.fetchOne(db, id: sender),
thread.variant == .contact
else { return } else { return }
_ = try Interaction( _ = try Interaction(
serverHash: message.serverHash, serverHash: message.serverHash,
threadId: thread.id, threadId: threadId,
authorId: sender, authorId: sender,
variant: { variant: {
switch messageKind { switch messageKind {

View File

@ -5,12 +5,16 @@ import GRDB
import SessionUtilitiesKit import SessionUtilitiesKit
extension MessageReceiver { extension MessageReceiver {
internal static func handleExpirationTimerUpdate(_ db: Database, message: ExpirationTimerUpdate) throws { internal static func handleExpirationTimerUpdate(
// Get the target thread _ db: Database,
threadId: String,
threadVariant: SessionThread.Variant,
message: ExpirationTimerUpdate
) throws {
guard guard
let targetId: String = MessageReceiver.threadInfo(db, message: message, openGroupId: nil)?.id, // Only process these for contact and legacy groups (new groups handle it separately)
let sender: String = message.sender, (threadVariant == .contact || threadVariant == .legacyGroup),
let thread: SessionThread = try? SessionThread.fetchOne(db, id: targetId) let sender: String = message.sender
else { return } else { return }
// Update the configuration // Update the configuration
@ -18,9 +22,10 @@ extension MessageReceiver {
// Note: Messages which had been sent during the previous configuration will still // Note: Messages which had been sent during the previous configuration will still
// use it's settings (so if you enable, send a message and then disable disappearing // use it's settings (so if you enable, send a message and then disable disappearing
// message then the message you had sent will still disappear) // message then the message you had sent will still disappear)
let config: DisappearingMessagesConfiguration = try thread.disappearingMessagesConfiguration let config: DisappearingMessagesConfiguration = try DisappearingMessagesConfiguration
.filter(id: threadId)
.fetchOne(db) .fetchOne(db)
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(thread.id)) .defaulting(to: DisappearingMessagesConfiguration.defaultWith(threadId))
.with( .with(
// If there is no duration then we should disable the expiration timer // If there is no duration then we should disable the expiration timer
isEnabled: ((message.duration ?? 0) > 0), isEnabled: ((message.duration ?? 0) > 0),
@ -33,7 +38,7 @@ extension MessageReceiver {
// Add an info message for the user // Add an info message for the user
_ = try Interaction( _ = try Interaction(
serverHash: nil, // Intentionally null so sync messages are seen as duplicates serverHash: nil, // Intentionally null so sync messages are seen as duplicates
threadId: thread.id, threadId: threadId,
authorId: sender, authorId: sender,
variant: .infoDisappearingMessagesUpdate, variant: .infoDisappearingMessagesUpdate,
body: config.messageInfoString( body: config.messageInfoString(

View File

@ -45,7 +45,8 @@ extension MessageReceiver {
} }
// Prep the unblinded thread // Prep the unblinded thread
let unblindedThread: SessionThread = try SessionThread.fetchOrCreate(db, id: senderId, variant: .contact) let unblindedThread: SessionThread = try SessionThread
.fetchOrCreate(db, id: senderId, variant: .contact, shouldBeVisible: nil)
// Need to handle a `MessageRequestResponse` sent to a blinded thread (ie. check if the sender matches // Need to handle a `MessageRequestResponse` sent to a blinded thread (ie. check if the sender matches
// the blinded ids of any threads) // the blinded ids of any threads)

View File

@ -5,17 +5,19 @@ import GRDB
import SessionUtilitiesKit import SessionUtilitiesKit
extension MessageReceiver { extension MessageReceiver {
internal static func handleTypingIndicator(_ db: Database, message: TypingIndicator) throws { internal static func handleTypingIndicator(
guard _ db: Database,
let senderPublicKey: String = message.sender, threadId: String,
let thread: SessionThread = try SessionThread.fetchOne(db, id: senderPublicKey) threadVariant: SessionThread.Variant,
else { return } message: TypingIndicator
) throws {
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else { return }
switch message.kind { switch message.kind {
case .started: case .started:
let needsToStartTypingIndicator: Bool = TypingIndicators.didStartTypingNeedsToStart( let needsToStartTypingIndicator: Bool = TypingIndicators.didStartTypingNeedsToStart(
threadId: thread.id, threadId: threadId,
threadVariant: thread.variant, threadVariant: threadVariant,
threadIsMessageRequest: thread.isMessageRequest(db), threadIsMessageRequest: thread.isMessageRequest(db),
direction: .incoming, direction: .incoming,
timestampMs: message.sentTimestamp.map { Int64($0) } timestampMs: message.sentTimestamp.map { Int64($0) }

View File

@ -6,7 +6,12 @@ import SessionSnodeKit
import SessionUtilitiesKit import SessionUtilitiesKit
extension MessageReceiver { extension MessageReceiver {
public static func handleUnsendRequest(_ db: Database, message: UnsendRequest) throws { public static func handleUnsendRequest(
_ db: Database,
threadId: String,
threadVariant: SessionThread.Variant,
message: UnsendRequest
) throws {
let userPublicKey: String = getUserHexEncodedPublicKey(db) let userPublicKey: String = getUserHexEncodedPublicKey(db)
guard message.sender == message.author || userPublicKey == message.sender else { return } guard message.sender == message.author || userPublicKey == message.sender else { return }
@ -19,12 +24,7 @@ extension MessageReceiver {
guard guard
let interactionId: Int64 = maybeInteraction?.id, let interactionId: Int64 = maybeInteraction?.id,
let interaction: Interaction = maybeInteraction, let interaction: Interaction = maybeInteraction
let threadVariant: SessionThread.Variant = try SessionThread
.filter(id: interaction.threadId)
.select(.variant)
.asRequest(of: SessionThread.Variant.self)
.fetchOne(db)
else { return } else { return }
// Mark incoming messages as read and remove any of their notifications // Mark incoming messages as read and remove any of their notifications

View File

@ -8,9 +8,10 @@ import SessionUtilitiesKit
extension MessageReceiver { extension MessageReceiver {
@discardableResult public static func handleVisibleMessage( @discardableResult public static func handleVisibleMessage(
_ db: Database, _ db: Database,
threadId: String,
threadVariant: SessionThread.Variant,
message: VisibleMessage, message: VisibleMessage,
associatedWithProto proto: SNProtoContent, associatedWithProto proto: SNProtoContent,
openGroupId: String?,
dependencies: Dependencies = Dependencies() dependencies: Dependencies = Dependencies()
) throws -> Int64 { ) throws -> Int64 {
guard let sender: String = message.sender, let dataMessage = proto.dataMessage else { guard let sender: String = message.sender, let dataMessage = proto.dataMessage else {
@ -53,8 +54,12 @@ extension MessageReceiver {
// Store the message variant so we can run variant-specific behaviours // Store the message variant so we can run variant-specific behaviours
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies) let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
let thread: SessionThread = try SessionThread let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: threadInfo.id, variant: threadInfo.variant) .fetchOrCreate(db, id: threadId, variant: threadVariant, shouldBeVisible: nil)
let maybeOpenGroup: OpenGroup? = openGroupId.map { try? OpenGroup.fetchOne(db, id: $0) } let maybeOpenGroup: OpenGroup? = {
guard threadVariant == .community else { return nil }
return try? OpenGroup.fetchOne(db, id: threadId)
}()
let variant: Interaction.Variant = { let variant: Interaction.Variant = {
guard guard
let senderSessionId: SessionId = SessionId(from: sender), let senderSessionId: SessionId = SessionId(from: sender),
@ -94,7 +99,14 @@ extension MessageReceiver {
}() }()
// Handle emoji reacts first (otherwise it's essentially an invalid message) // Handle emoji reacts first (otherwise it's essentially an invalid message)
if let interactionId: Int64 = try handleEmojiReactIfNeeded(db, message: message, associatedWithProto: proto, sender: sender, messageSentTimestamp: messageSentTimestamp, openGroupId: openGroupId, thread: thread) { if let interactionId: Int64 = try handleEmojiReactIfNeeded(
db,
thread: thread,
message: message,
associatedWithProto: proto,
sender: sender,
messageSentTimestamp: messageSentTimestamp
) {
return interactionId return interactionId
} }
@ -314,12 +326,11 @@ extension MessageReceiver {
private static func handleEmojiReactIfNeeded( private static func handleEmojiReactIfNeeded(
_ db: Database, _ db: Database,
thread: SessionThread,
message: VisibleMessage, message: VisibleMessage,
associatedWithProto proto: SNProtoContent, associatedWithProto proto: SNProtoContent,
sender: String, sender: String,
messageSentTimestamp: TimeInterval, messageSentTimestamp: TimeInterval
openGroupId: String?,
thread: SessionThread
) throws -> Int64? { ) throws -> Int64? {
guard guard
let reaction: VisibleMessage.VMReaction = message.reaction, let reaction: VisibleMessage.VMReaction = message.reaction,
@ -347,7 +358,7 @@ extension MessageReceiver {
switch reaction.kind { switch reaction.kind {
case .react: case .react:
let reaction = Reaction( let reaction: Reaction = try Reaction(
interactionId: interactionId, interactionId: interactionId,
serverHash: message.serverHash, serverHash: message.serverHash,
timestampMs: Int64(messageSentTimestamp * 1000), timestampMs: Int64(messageSentTimestamp * 1000),
@ -355,8 +366,8 @@ extension MessageReceiver {
emoji: reaction.emoji, emoji: reaction.emoji,
count: 1, count: 1,
sortId: sortId sortId: sortId
) ).inserted(db)
try reaction.insert(db)
if sender != getUserHexEncodedPublicKey(db) { if sender != getUserHexEncodedPublicKey(db) {
Environment.shared?.notificationsManager.wrappedValue? Environment.shared?.notificationsManager.wrappedValue?
.notifyUser( .notifyUser(

View File

@ -15,7 +15,7 @@ extension MessageSender {
_ db: Database, _ db: Database,
name: String, name: String,
members: Set<String> members: Set<String>
) -> AnyPublisher<SessionThread, Error> { ) throws -> AnyPublisher<SessionThread, Error> {
let userPublicKey: String = getUserHexEncodedPublicKey(db) let userPublicKey: String = getUserHexEncodedPublicKey(db)
var members: Set<String> = members var members: Set<String> = members
@ -30,89 +30,85 @@ extension MessageSender {
// Create the group // Create the group
members.insert(userPublicKey) // Ensure the current user is included in the member list members.insert(userPublicKey) // Ensure the current user is included in the member list
let membersAsData = members.map { Data(hex: $0) } let membersAsData: [Data] = members.map { Data(hex: $0) }
let admins = [ userPublicKey ] let admins: Set<String> = [ userPublicKey ]
let adminsAsData = admins.map { Data(hex: $0) } let adminsAsData: [Data] = admins.map { Data(hex: $0) }
let formationTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) let formationTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
let thread: SessionThread
let memberSendData: [MessageSender.PreparedSendData]
do { // Create the relevant objects in the database
// Create the relevant objects in the database let thread: SessionThread = try SessionThread
thread = try SessionThread .fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup, shouldBeVisible: true)
.fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup) try ClosedGroup(
try ClosedGroup( threadId: groupPublicKey,
threadId: groupPublicKey, name: name,
name: name, formationTimestamp: formationTimestamp
formationTimestamp: formationTimestamp ).insert(db)
// Store the key pair
let latestKeyPairReceivedTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
try ClosedGroupKeyPair(
threadId: groupPublicKey,
publicKey: encryptionKeyPair.publicKey,
secretKey: encryptionKeyPair.privateKey,
receivedTimestamp: latestKeyPairReceivedTimestamp
).insert(db)
// Create the member objects
try admins.forEach { adminId in
try GroupMember(
groupId: groupPublicKey,
profileId: adminId,
role: .admin,
isHidden: false
).insert(db) ).insert(db)
}
// Store the key pair
try ClosedGroupKeyPair( try members.forEach { memberId in
threadId: groupPublicKey, try GroupMember(
publicKey: encryptionKeyPair.publicKey, groupId: groupPublicKey,
secretKey: encryptionKeyPair.privateKey, profileId: memberId,
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) role: .standard,
isHidden: false
).insert(db) ).insert(db)
}
// Create the member objects
try admins.forEach { adminId in // Update libSession
try GroupMember( try SessionUtil.add(
groupId: groupPublicKey, db,
profileId: adminId, groupPublicKey: groupPublicKey,
role: .admin, name: name,
isHidden: false latestKeyPairPublicKey: encryptionKeyPair.publicKey,
).insert(db) latestKeyPairSecretKey: encryptionKeyPair.privateKey,
} latestKeyPairReceivedTimestamp: latestKeyPairReceivedTimestamp,
members: members,
try members.forEach { memberId in admins: admins
try GroupMember( )
groupId: groupPublicKey,
profileId: memberId, let memberSendData: [MessageSender.PreparedSendData] = try members
role: .standard, .map { memberId -> MessageSender.PreparedSendData in
isHidden: false try MessageSender.preparedSendData(
).insert(db) db,
} message: ClosedGroupControlMessage(
kind: .new(
// Notify the user publicKey: Data(hex: groupPublicKey),
// name: name,
// Note: Intentionally don't want a 'serverHash' for closed group creation encryptionKeyPair: Box.KeyPair(
_ = try Interaction( publicKey: encryptionKeyPair.publicKey.bytes,
threadId: thread.id, secretKey: encryptionKeyPair.privateKey.bytes
authorId: userPublicKey,
variant: .infoClosedGroupCreated,
timestampMs: SnodeAPI.currentOffsetTimestampMs()
).inserted(db)
memberSendData = try members
.map { memberId -> MessageSender.PreparedSendData in
try MessageSender.preparedSendData(
db,
message: ClosedGroupControlMessage(
kind: .new(
publicKey: Data(hex: groupPublicKey),
name: name,
encryptionKeyPair: Box.KeyPair(
publicKey: encryptionKeyPair.publicKey.bytes,
secretKey: encryptionKeyPair.privateKey.bytes
),
members: membersAsData,
admins: adminsAsData,
expirationTimer: 0
), ),
// Note: We set this here to ensure the value matches members: membersAsData,
// the 'ClosedGroup' object we created admins: adminsAsData,
sentTimestampMs: UInt64(floor(formationTimestamp * 1000)) expirationTimer: 0
), ),
to: .contact(publicKey: memberId), // Note: We set this here to ensure the value matches
interactionId: nil // the 'ClosedGroup' object we created
) sentTimestampMs: UInt64(floor(formationTimestamp * 1000))
} ),
} to: .contact(publicKey: memberId),
catch { namespace: Message.Destination.contact(publicKey: memberId).defaultNamespace,
return Fail(error: error) interactionId: nil
.eraseToAnyPublisher() )
} }
return Publishers return Publishers
.MergeMany( .MergeMany(
@ -207,6 +203,7 @@ extension MessageSender {
) )
), ),
to: try Message.Destination.from(db, thread: thread), to: try Message.Destination.from(db, thread: thread),
namespace: try Message.Destination.from(db, thread: thread).defaultNamespace,
interactionId: nil interactionId: nil
) )
} }
@ -225,6 +222,21 @@ extension MessageSender {
try newKeyPair.insert(db) try newKeyPair.insert(db)
} }
// Update libSession
try? SessionUtil.update(
db,
groupPublicKey: closedGroup.threadId,
latestKeyPair: newKeyPair,
members: allGroupMembers
.filter { $0.role == .standard || $0.role == .zombie }
.map { $0.profileId }
.asSet(),
admins: allGroupMembers
.filter { $0.role == .admin }
.map { $0.profileId }
.asSet()
)
distributingKeyPairs.mutate { distributingKeyPairs.mutate {
if let index = ($0[closedGroup.id] ?? []).firstIndex(of: newKeyPair) { if let index = ($0[closedGroup.id] ?? []).firstIndex(of: newKeyPair) {
$0[closedGroup.id] = ($0[closedGroup.id] ?? []) $0[closedGroup.id] = ($0[closedGroup.id] ?? [])
@ -284,6 +296,13 @@ extension MessageSender {
interactionId: interactionId, interactionId: interactionId,
in: thread in: thread
) )
// Update libSession
try? SessionUtil.update(
db,
groupPublicKey: closedGroup.threadId,
name: name
)
} }
} }
catch { catch {
@ -386,6 +405,20 @@ extension MessageSender {
guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved } guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved }
// Update libSession
try? SessionUtil.update(
db,
groupPublicKey: closedGroup.threadId,
members: allGroupMembers
.filter { $0.role == .standard || $0.role == .zombie }
.map { $0.profileId }
.asSet(),
admins: allGroupMembers
.filter { $0.role == .admin }
.map { $0.profileId }
.asSet()
)
// Send the update to the group // Send the update to the group
try MessageSender.send( try MessageSender.send(
db, db,
@ -399,7 +432,7 @@ extension MessageSender {
try addedMembers.forEach { member in try addedMembers.forEach { member in
// Send updates to the new members individually // Send updates to the new members individually
let thread: SessionThread = try SessionThread let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: member, variant: .contact) .fetchOrCreate(db, id: member, variant: .contact, shouldBeVisible: nil)
try MessageSender.send( try MessageSender.send(
db, db,
@ -505,6 +538,7 @@ extension MessageSender {
) )
), ),
to: try Message.Destination.from(db, thread: thread), to: try Message.Destination.from(db, thread: thread),
namespace: try Message.Destination.from(db, thread: thread).defaultNamespace,
interactionId: interactionId interactionId: interactionId
) )
) )
@ -571,6 +605,7 @@ extension MessageSender {
kind: .memberLeft kind: .memberLeft
), ),
to: try Message.Destination.from(db, thread: thread), to: try Message.Destination.from(db, thread: thread),
namespace: try Message.Destination.from(db, thread: thread).defaultNamespace,
interactionId: interactionId interactionId: interactionId
) )
@ -594,6 +629,12 @@ extension MessageSender {
} }
} }
catch { catch {
try? ClosedGroup.removeKeysAndUnsubscribe(
db,
threadId: groupPublicKey,
removeGroupData: false,
calledFromConfigHandling: false
)
return Fail(error: error) return Fail(error: error)
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
@ -651,7 +692,7 @@ extension MessageSender {
).build() ).build()
let plaintext = try proto.serializedData() let plaintext = try proto.serializedData()
let thread: SessionThread = try SessionThread let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: publicKey, variant: .contact) .fetchOrCreate(db, id: publicKey, variant: .contact, shouldBeVisible: nil)
let ciphertext = try MessageSender.encryptWithSessionProtocol( let ciphertext = try MessageSender.encryptWithSessionProtocol(
db, db,
plaintext: plaintext, plaintext: plaintext,

View File

@ -19,7 +19,7 @@ public enum MessageReceiver {
isOutgoing: Bool? = nil, isOutgoing: Bool? = nil,
otherBlindedPublicKey: String? = nil, otherBlindedPublicKey: String? = nil,
dependencies: SMKDependencies = SMKDependencies() dependencies: SMKDependencies = SMKDependencies()
) throws -> (Message, SNProtoContent, String) { ) throws -> (Message, SNProtoContent, String, SessionThread.Variant) {
let userPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies) let userPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
let isOpenGroupMessage: Bool = (openGroupId != nil) let isOpenGroupMessage: Bool = (openGroupId != nil)
@ -147,7 +147,6 @@ public enum MessageReceiver {
message.recipient = userPublicKey message.recipient = userPublicKey
message.sentTimestamp = envelope.timestamp message.sentTimestamp = envelope.timestamp
message.receivedTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs()) message.receivedTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs())
message.groupPublicKey = groupPublicKey
message.openGroupServerMessageId = openGroupMessageServerId.map { UInt64($0) } message.openGroupServerMessageId = openGroupMessageServerId.map { UInt64($0) }
// Validate // Validate
@ -161,28 +160,29 @@ public enum MessageReceiver {
} }
// Extract the proper threadId for the message // Extract the proper threadId for the message
let threadId: String = { let (threadId, threadVariant): (String, SessionThread.Variant) = {
if let groupPublicKey: String = groupPublicKey { return groupPublicKey } if let groupPublicKey: String = groupPublicKey { return (groupPublicKey, .legacyGroup) }
if let openGroupId: String = openGroupId { return openGroupId } if let openGroupId: String = openGroupId { return (openGroupId, .community) }
switch message { switch message {
case let message as VisibleMessage: return (message.syncTarget ?? sender) case let message as VisibleMessage: return ((message.syncTarget ?? sender), .contact)
case let message as ExpirationTimerUpdate: return (message.syncTarget ?? sender) case let message as ExpirationTimerUpdate: return ((message.syncTarget ?? sender), .contact)
default: return sender default: return (sender, .contact)
} }
}() }()
return (message, proto, threadId) return (message, proto, threadId, threadVariant)
} }
// MARK: - Handling // MARK: - Handling
public static func handle( public static func handle(
_ db: Database, _ db: Database,
threadId: String,
threadVariant: SessionThread.Variant,
message: Message, message: Message,
serverExpirationTimestamp: TimeInterval?, serverExpirationTimestamp: TimeInterval?,
associatedWithProto proto: SNProtoContent, associatedWithProto proto: SNProtoContent,
openGroupId: String?,
dependencies: SMKDependencies = SMKDependencies() dependencies: SMKDependencies = SMKDependencies()
) throws { ) throws {
switch message { switch message {
@ -194,35 +194,70 @@ public enum MessageReceiver {
) )
case let message as TypingIndicator: case let message as TypingIndicator:
try MessageReceiver.handleTypingIndicator(db, message: message) try MessageReceiver.handleTypingIndicator(
db,
threadId: threadId,
threadVariant: threadVariant,
message: message
)
case let message as ClosedGroupControlMessage: case let message as ClosedGroupControlMessage:
try MessageReceiver.handleClosedGroupControlMessage(db, message) try MessageReceiver.handleClosedGroupControlMessage(
db,
threadId: threadId,
threadVariant: threadVariant,
message: message
)
case let message as DataExtractionNotification: case let message as DataExtractionNotification:
try MessageReceiver.handleDataExtractionNotification(db, message: message) try MessageReceiver.handleDataExtractionNotification(
db,
threadId: threadId,
threadVariant: threadVariant,
message: message
)
case let message as ExpirationTimerUpdate: case let message as ExpirationTimerUpdate:
try MessageReceiver.handleExpirationTimerUpdate(db, message: message) try MessageReceiver.handleExpirationTimerUpdate(
db,
threadId: threadId,
threadVariant: threadVariant,
message: message
)
case let message as ConfigurationMessage: case let message as ConfigurationMessage:
try MessageReceiver.handleConfigurationMessage(db, message: message) try MessageReceiver.handleConfigurationMessage(db, message: message)
case let message as UnsendRequest: case let message as UnsendRequest:
try MessageReceiver.handleUnsendRequest(db, message: message) try MessageReceiver.handleUnsendRequest(
db,
threadId: threadId,
threadVariant: threadVariant,
message: message
)
case let message as CallMessage: case let message as CallMessage:
try MessageReceiver.handleCallMessage(db, message: message) try MessageReceiver.handleCallMessage(
db,
threadId: threadId,
threadVariant: threadVariant,
message: message
)
case let message as MessageRequestResponse: case let message as MessageRequestResponse:
try MessageReceiver.handleMessageRequestResponse(db, message: message, dependencies: dependencies) try MessageReceiver.handleMessageRequestResponse(
db,
message: message,
dependencies: dependencies
)
case let message as VisibleMessage: case let message as VisibleMessage:
try MessageReceiver.handleVisibleMessage( try MessageReceiver.handleVisibleMessage(
db, db,
threadId: threadId,
threadVariant: threadVariant,
message: message, message: message,
associatedWithProto: proto, associatedWithProto: proto
openGroupId: openGroupId
) )
// SharedConfigMessages should be handled by the 'SharedUtil' instead of this // SharedConfigMessages should be handled by the 'SharedUtil' instead of this
@ -232,13 +267,13 @@ public enum MessageReceiver {
} }
// Perform any required post-handling logic // Perform any required post-handling logic
try MessageReceiver.postHandleMessage(db, message: message, openGroupId: openGroupId) try MessageReceiver.postHandleMessage(db, threadId: threadId, message: message)
} }
public static func postHandleMessage( public static func postHandleMessage(
_ db: Database, _ db: Database,
message: Message, threadId: String,
openGroupId: String? message: Message
) throws { ) throws {
// When handling any non-typing indicator message we want to make sure the thread becomes // When handling any non-typing indicator message we want to make sure the thread becomes
// visible (the only other spot this flag gets set is when sending messages) // visible (the only other spot this flag gets set is when sending messages)
@ -246,14 +281,12 @@ public enum MessageReceiver {
case is TypingIndicator: break case is TypingIndicator: break
default: default:
guard let threadInfo: (id: String, variant: SessionThread.Variant) = threadInfo(db, message: message, openGroupId: openGroupId) else { try SessionThread
return .filter(id: threadId)
} .updateAllAndConfig(
db,
_ = try SessionThread SessionThread.Columns.shouldBeVisible.set(to: true)
.fetchOrCreate(db, id: threadInfo.id, variant: threadInfo.variant) )
.with(shouldBeVisible: true)
.saved(db)
} }
} }
@ -281,36 +314,4 @@ public enum MessageReceiver {
try reaction.with(interactionId: interactionId).insert(db) try reaction.with(interactionId: interactionId).insert(db)
} }
} }
// MARK: - Convenience
internal static func threadInfo(_ db: Database, message: Message, openGroupId: String?) -> (id: String, variant: SessionThread.Variant)? {
if let openGroupId: String = openGroupId {
// Note: We don't want to create a thread for an open group if it doesn't exist
if (try? SessionThread.exists(db, id: openGroupId)) != true { return nil }
return (openGroupId, .community)
}
if let groupPublicKey: String = message.groupPublicKey {
// Note: We don't want to create a thread for a closed group if it doesn't exist
if (try? SessionThread.exists(db, id: groupPublicKey)) != true { return nil }
return (groupPublicKey, .legacyGroup)
}
// Extract the 'syncTarget' value if there is one
let maybeSyncTarget: String?
switch message {
case let message as VisibleMessage: maybeSyncTarget = message.syncTarget
case let message as ExpirationTimerUpdate: maybeSyncTarget = message.syncTarget
default: maybeSyncTarget = nil
}
// Note: We don't want to create a thread for a closed group if it doesn't exist
guard let contactId: String = (maybeSyncTarget ?? message.sender) else { return nil }
return (contactId, .contact)
}
} }

View File

@ -284,7 +284,7 @@ public class Poller {
return nil return nil
} }
} }
.grouped { threadId, _, _ in (threadId ?? Message.nonThreadMessageId) } .grouped { threadId, _, _, _ in threadId }
.forEach { threadId, threadMessages in .forEach { threadId, threadMessages in
messageCount += threadMessages.count messageCount += threadMessages.count
processedMessages += threadMessages.map { $0.messageInfo.message } processedMessages += threadMessages.map { $0.messageInfo.message }

View File

@ -787,7 +787,7 @@ public extension SessionThreadViewModel {
let thread: TypedTableAlias<SessionThread> = TypedTableAlias() let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = TypedTableAlias() let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
return SQL("\(thread[.isPinned]) DESC, IFNULL(\(interaction[.timestampMs]), (\(thread[.creationDateTimestamp]) * 1000)) DESC") return SQL("(IFNULL(\(thread[.pinnedPriority]), 0) > 0) DESC, IFNULL(\(interaction[.timestampMs]), (\(thread[.creationDateTimestamp]) * 1000)) DESC")
}() }()
static let messageRequetsOrderSQL: SQL = { static let messageRequetsOrderSQL: SQL = {

View File

@ -206,12 +206,13 @@ class ConfigUserGroupsSpec {
.to(equal("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")) .to(equal("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"))
// The new data doesn't get stored until we call this: // The new data doesn't get stored until we call this:
user_groups_set_legacy_group(conf, legacyGroup2) user_groups_set_free_legacy_group(conf, legacyGroup2)
let legacyGroup3: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf, &cDefinitelyRealId) let legacyGroup3: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf, &cDefinitelyRealId)
expect(legacyGroup3?.pointee).toNot(beNil()) expect(legacyGroup3?.pointee).toNot(beNil())
expect(config_needs_push(conf)).to(beTrue()) expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue()) expect(config_needs_dump(conf)).to(beTrue())
ugroups_legacy_group_free(legacyGroup3)
let communityPubkey: String = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" let communityPubkey: String = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
var cCommunityPubkey: [UInt8] = Data(hex: communityPubkey).cArray var cCommunityPubkey: [UInt8] = Data(hex: communityPubkey).cArray
@ -307,6 +308,7 @@ class ConfigUserGroupsSpec {
} }
ugroups_legacy_members_free(membersIt3) ugroups_legacy_members_free(membersIt3)
ugroups_legacy_group_free(legacyGroup4)
expect(membersSeen3).to(equal([ expect(membersSeen3).to(equal([
"050000000000000000000000000000000000000000000000000000000000000000": false, "050000000000000000000000000000000000000000000000000000000000000000": false,
@ -436,7 +438,7 @@ class ConfigUserGroupsSpec {
expect(config_needs_dump(conf2)).to(beFalse()) expect(config_needs_dump(conf2)).to(beFalse())
pushData9.deallocate() pushData9.deallocate()
user_groups_set_legacy_group(conf2, legacyGroup5) user_groups_set_free_legacy_group(conf2, legacyGroup5)
expect(config_needs_push(conf2)).to(beTrue()) expect(config_needs_push(conf2)).to(beTrue())
expect(config_needs_dump(conf2)).to(beTrue()) expect(config_needs_dump(conf2)).to(beTrue())

View File

@ -75,23 +75,14 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
return return
} }
let maybeVariant: SessionThread.Variant? = processedMessage.threadId
.map { threadId in
try? SessionThread
.filter(id: threadId)
.select(.variant)
.asRequest(of: SessionThread.Variant.self)
.fetchOne(db)
}
let isOpenGroup: Bool = (maybeVariant == .community)
switch processedMessage.messageInfo.message { switch processedMessage.messageInfo.message {
case let visibleMessage as VisibleMessage: case let visibleMessage as VisibleMessage:
let interactionId: Int64 = try MessageReceiver.handleVisibleMessage( let interactionId: Int64 = try MessageReceiver.handleVisibleMessage(
db, db,
threadId: processedMessage.threadId,
threadVariant: processedMessage.threadVariant,
message: visibleMessage, message: visibleMessage,
associatedWithProto: processedMessage.proto, associatedWithProto: processedMessage.proto
openGroupId: (isOpenGroup ? processedMessage.threadId : nil)
) )
// Remove the notifications if there is an outgoing messages from a linked device // Remove the notifications if there is an outgoing messages from a linked device
@ -111,19 +102,40 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
} }
case let unsendRequest as UnsendRequest: case let unsendRequest as UnsendRequest:
try MessageReceiver.handleUnsendRequest(db, message: unsendRequest) try MessageReceiver.handleUnsendRequest(
db,
threadId: processedMessage.threadId,
threadVariant: processedMessage.threadVariant,
message: unsendRequest
)
case let closedGroupControlMessage as ClosedGroupControlMessage: case let closedGroupControlMessage as ClosedGroupControlMessage:
try MessageReceiver.handleClosedGroupControlMessage(db, closedGroupControlMessage) try MessageReceiver.handleClosedGroupControlMessage(
db,
threadId: processedMessage.threadId,
threadVariant: processedMessage.threadVariant,
message: closedGroupControlMessage
)
case let callMessage as CallMessage: case let callMessage as CallMessage:
try MessageReceiver.handleCallMessage(db, message: callMessage) try MessageReceiver.handleCallMessage(
db,
threadId: processedMessage.threadId,
threadVariant: processedMessage.threadVariant,
message: callMessage
)
guard case .preOffer = callMessage.kind else { return self.completeSilenty() } guard case .preOffer = callMessage.kind else { return self.completeSilenty() }
if !db[.areCallsEnabled] { if !db[.areCallsEnabled] {
if let sender: String = callMessage.sender, let interaction: Interaction = try MessageReceiver.insertCallInfoMessage(db, for: callMessage, state: .permissionDenied) { if let sender: String = callMessage.sender, let interaction: Interaction = try MessageReceiver.insertCallInfoMessage(db, for: callMessage, state: .permissionDenied) {
let thread: SessionThread = try SessionThread.fetchOrCreate(db, id: sender, variant: .contact) let thread: SessionThread = try SessionThread
.fetchOrCreate(
db,
id: sender,
variant: .contact,
shouldBeVisible: nil
)
Environment.shared?.notificationsManager.wrappedValue? Environment.shared?.notificationsManager.wrappedValue?
.notifyUser( .notifyUser(
@ -146,7 +158,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
try SessionUtil.handleConfigMessages( try SessionUtil.handleConfigMessages(
db, db,
messages: [sharedConfigMessage], messages: [sharedConfigMessage],
publicKey: (processedMessage.threadId ?? "") publicKey: processedMessage.threadId
) )
default: break default: break
@ -155,8 +167,8 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
// Perform any required post-handling logic // Perform any required post-handling logic
try MessageReceiver.postHandleMessage( try MessageReceiver.postHandleMessage(
db, db,
message: processedMessage.messageInfo.message, threadId: processedMessage.threadId,
openGroupId: (isOpenGroup ? processedMessage.threadId : nil) message: processedMessage.messageInfo.message
) )
} }
catch { catch {