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:
parent
8eed08b5b4
commit
7ee84fe0d3
|
@ -457,7 +457,7 @@ extension ConversationVC:
|
|||
// Update the thread to be visible
|
||||
_ = try SessionThread
|
||||
.filter(id: threadId)
|
||||
.updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||
|
||||
let authorId: String = {
|
||||
if let blindedId = self?.viewModel.threadData.currentUserBlindedPublicKey {
|
||||
|
@ -588,7 +588,7 @@ extension ConversationVC:
|
|||
// Update the thread to be visible
|
||||
_ = try SessionThread
|
||||
.filter(id: threadId)
|
||||
.updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||
|
||||
// Create the interaction
|
||||
let interaction: Interaction = try Interaction(
|
||||
|
@ -1104,7 +1104,8 @@ extension ConversationVC:
|
|||
guard viewModel.threadData.canWrite else { return }
|
||||
guard SessionId.Prefix(from: sessionId) == .blinded else {
|
||||
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)
|
||||
|
@ -1130,7 +1131,12 @@ extension ConversationVC:
|
|||
)
|
||||
|
||||
return try SessionThread
|
||||
.fetchOrCreate(db, id: (lookup.sessionId ?? lookup.blindedId), variant: .contact)
|
||||
.fetchOrCreate(
|
||||
db,
|
||||
id: (lookup.sessionId ?? lookup.blindedId),
|
||||
variant: .contact,
|
||||
shouldBeVisible: nil
|
||||
)
|
||||
.id
|
||||
}
|
||||
|
||||
|
@ -1308,7 +1314,7 @@ extension ConversationVC:
|
|||
// Update the thread to be visible
|
||||
_ = try SessionThread
|
||||
.filter(id: thread.id)
|
||||
.updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||
|
||||
let pendingReaction: Reaction? = {
|
||||
if remove {
|
||||
|
|
|
@ -206,6 +206,35 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
|
||||
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 = {
|
||||
let result: UIStackView = UIStackView()
|
||||
|
@ -367,8 +396,13 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
|
||||
// Message requests view & scroll to bottom
|
||||
view.addSubview(scrollButton)
|
||||
view.addSubview(emptyStateLabel)
|
||||
view.addSubview(messageRequestBackgroundView)
|
||||
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(messageRequestDescriptionContainerView)
|
||||
|
@ -376,7 +410,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
messageRequestDescriptionContainerView.addSubview(messageRequestDescriptionLabel)
|
||||
messageRequestActionStackView.addArrangedSubview(messageRequestAcceptButton)
|
||||
messageRequestActionStackView.addArrangedSubview(messageRequestDeleteButton)
|
||||
|
||||
|
||||
scrollButton.pin(.trailing, to: .trailing, of: view, withInset: -20)
|
||||
messageRequestStackView.pin(.leading, to: .leading, 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),
|
||||
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
|
||||
|
@ -718,6 +765,19 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
self.hasLoadedInitialInteractionData = true
|
||||
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 {
|
||||
self.tableView.reloadData()
|
||||
self.performInitialScrollIfNeeded()
|
||||
|
@ -726,6 +786,14 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
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)
|
||||
if let messageUpdates: [MessageViewModel] = updatedData.first(where: { $0.model == .messages })?.elements {
|
||||
self.currentReactionListSheet?.handleInteractionUpdates(messageUpdates)
|
||||
|
|
|
@ -6,7 +6,7 @@ import SessionMessagingKit
|
|||
|
||||
final class InfoMessageCell: MessageCell {
|
||||
private static let iconSize: CGFloat = 16
|
||||
private static let inset = Values.mediumSpacing
|
||||
public static let inset = Values.mediumSpacing
|
||||
|
||||
private var isHandlingLongPress: Bool = false
|
||||
|
||||
|
|
|
@ -714,7 +714,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
|
||||
dependencies.storage.writeAsync { db 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(
|
||||
url: communityUrl,
|
||||
|
|
|
@ -770,20 +770,13 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
|||
// Delay the change to give the cell "unswipe" animation some time to complete
|
||||
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) {
|
||||
Storage.shared.writeAsync { db in
|
||||
// If we are unpinning then just clear the value
|
||||
guard threadViewModel.threadPinnedPriority == 0 else {
|
||||
try SessionThread
|
||||
.filter(id: threadViewModel.threadId)
|
||||
.updateAllAndConfig(
|
||||
db,
|
||||
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)
|
||||
try SessionThread
|
||||
.filter(id: threadViewModel.threadId)
|
||||
.updateAllAndConfig(
|
||||
db,
|
||||
SessionThread.Columns.pinnedPriority
|
||||
.set(to: (threadViewModel.threadPinnedPriority == 0 ? 1 : 0))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -368,6 +368,10 @@ public class HomeViewModel {
|
|||
public func delete(threadId: String, threadVariant: SessionThread.Variant) {
|
||||
Storage.shared.writeAsync { db in
|
||||
switch threadVariant {
|
||||
case .contact:
|
||||
try SessionUtil
|
||||
.hide(db, contactIds: [threadId])
|
||||
|
||||
case .legacyGroup, .group:
|
||||
MessageSender
|
||||
.leave(db, groupPublicKey: threadId)
|
||||
|
@ -379,8 +383,6 @@ public class HomeViewModel {
|
|||
openGroupId: threadId,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
_ = try SessionThread
|
||||
|
|
|
@ -179,7 +179,8 @@ final class NewConversationVC: BaseVC, ThemedNavigation, UITableViewDelegate, UI
|
|||
|
||||
let sessionId = newConversationViewModel.sectionData[indexPath.section].contacts[indexPath.row].id
|
||||
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 }
|
||||
|
|
|
@ -233,7 +233,8 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle
|
|||
|
||||
private func startNewDM(with sessionId: String) {
|
||||
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 }
|
||||
|
|
|
@ -12,7 +12,8 @@ public struct SessionApp {
|
|||
|
||||
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 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))
|
||||
}
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -613,3 +613,4 @@
|
|||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
"MARK_AS_READ" = "Mark Read";
|
||||
"MARK_AS_UNREAD" = "Mark Unread";
|
||||
"GROUP_CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
|
||||
|
|
|
@ -304,7 +304,8 @@ public enum PushRegistrationError: Error {
|
|||
}()
|
||||
|
||||
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(
|
||||
messageUuid: uuid,
|
||||
|
|
|
@ -151,8 +151,7 @@ enum Onboarding {
|
|||
|
||||
// Create the 'Note to Self' thread (not visible by default)
|
||||
try SessionThread
|
||||
.fetchOrCreate(db, id: x25519PublicKey, variant: .contact)
|
||||
.save(db)
|
||||
.fetchOrCreate(db, id: x25519PublicKey, variant: .contact, shouldBeVisible: false)
|
||||
}
|
||||
|
||||
// Set hasSyncedInitialConfiguration to true so that when we hit the
|
||||
|
|
|
@ -139,7 +139,8 @@ final class QRCodeVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControl
|
|||
}
|
||||
else {
|
||||
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 }
|
||||
|
|
|
@ -108,7 +108,8 @@ enum MockDataGenerator {
|
|||
logProgress("", "Start")
|
||||
|
||||
// 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
|
||||
|
||||
|
@ -133,9 +134,12 @@ enum MockDataGenerator {
|
|||
|
||||
// Generate the thread
|
||||
let thread: SessionThread = try! SessionThread
|
||||
.fetchOrCreate(db, id: randomSessionId, variant: .contact)
|
||||
.with(shouldBeVisible: true)
|
||||
.saved(db)
|
||||
.fetchOrCreate(
|
||||
db,
|
||||
id: randomSessionId,
|
||||
variant: .contact,
|
||||
shouldBeVisible: true
|
||||
)
|
||||
|
||||
// Generate the contact
|
||||
let contact: Contact = try! Contact(
|
||||
|
@ -241,9 +245,12 @@ enum MockDataGenerator {
|
|||
}
|
||||
|
||||
let thread: SessionThread = try! SessionThread
|
||||
.fetchOrCreate(db, id: randomGroupPublicKey, variant: .legacyGroup)
|
||||
.with(shouldBeVisible: true)
|
||||
.saved(db)
|
||||
.fetchOrCreate(
|
||||
db,
|
||||
id: randomGroupPublicKey,
|
||||
variant: .legacyGroup,
|
||||
shouldBeVisible: true
|
||||
)
|
||||
_ = try! ClosedGroup(
|
||||
threadId: randomGroupPublicKey,
|
||||
name: groupName,
|
||||
|
@ -367,9 +374,12 @@ enum MockDataGenerator {
|
|||
|
||||
// Create the open group model and the thread
|
||||
let thread: SessionThread = try! SessionThread
|
||||
.fetchOrCreate(db, id: randomGroupPublicKey, variant: .community)
|
||||
.with(shouldBeVisible: true)
|
||||
.saved(db)
|
||||
.fetchOrCreate(
|
||||
db,
|
||||
id: randomGroupPublicKey,
|
||||
variant: .community,
|
||||
shouldBeVisible: true
|
||||
)
|
||||
_ = try! OpenGroup(
|
||||
server: serverName,
|
||||
roomToken: roomName,
|
||||
|
|
|
@ -179,12 +179,10 @@ public enum SMKLegacy {
|
|||
internal func toNonLegacy(_ instance: Message? = nil) throws -> Message {
|
||||
let result: Message = (instance ?? Message())
|
||||
result.id = self.id
|
||||
result.threadId = self.threadID
|
||||
result.sentTimestamp = self.sentTimestamp
|
||||
result.receivedTimestamp = self.receivedTimestamp
|
||||
result.recipient = self.recipient
|
||||
result.sender = self.sender
|
||||
result.groupPublicKey = self.groupPublicKey
|
||||
result.openGroupServerMessageId = self.openGroupServerMessageID
|
||||
result.serverHash = self.serverHash
|
||||
|
||||
|
|
|
@ -57,12 +57,9 @@ enum _012_SharedUtilChanges: Migration {
|
|||
|
||||
// MARK: - Shared Data
|
||||
|
||||
let pinnedThreadIds: [String] = try SessionThread
|
||||
.select(SessionThread.Columns.id)
|
||||
.filter(SessionThread.Columns.isPinned)
|
||||
.order(Column.rowID)
|
||||
.asRequest(of: String.self)
|
||||
let allThreads: [String: SessionThread] = try SessionThread
|
||||
.fetchAll(db)
|
||||
.reduce(into: [:]) { result, next in result[next.id] = next }
|
||||
|
||||
// MARK: - UserProfile Config Dump
|
||||
|
||||
|
@ -71,12 +68,12 @@ enum _012_SharedUtilChanges: Migration {
|
|||
secretKey: secretKey,
|
||||
cachedData: nil
|
||||
)
|
||||
let userProfileConfResult: SessionUtil.ConfResult = try SessionUtil.update(
|
||||
try SessionUtil.update(
|
||||
profile: Profile.fetchOrCreateCurrentUser(db),
|
||||
in: userProfileConf
|
||||
)
|
||||
|
||||
if userProfileConfResult.needsDump {
|
||||
if config_needs_dump(userProfileConf) {
|
||||
try SessionUtil
|
||||
.createDump(
|
||||
conf: userProfileConf,
|
||||
|
@ -89,6 +86,10 @@ enum _012_SharedUtilChanges: Migration {
|
|||
// MARK: - Contact Config Dump
|
||||
|
||||
let contactsData: [ContactInfo] = try Contact
|
||||
.filter(
|
||||
Contact.Columns.isBlocked == true ||
|
||||
allThreads.keys.contains(Contact.Columns.id)
|
||||
)
|
||||
.including(optional: Contact.profile)
|
||||
.asRequest(of: ContactInfo.self)
|
||||
.fetchAll(db)
|
||||
|
@ -98,21 +99,21 @@ enum _012_SharedUtilChanges: Migration {
|
|||
secretKey: secretKey,
|
||||
cachedData: nil
|
||||
)
|
||||
let contactsConfResult: SessionUtil.ConfResult = try SessionUtil.upsert(
|
||||
try SessionUtil.upsert(
|
||||
contactData: contactsData
|
||||
.map { data in
|
||||
(
|
||||
data.contact.id,
|
||||
data.contact,
|
||||
data.profile,
|
||||
Int32(pinnedThreadIds.firstIndex(of: data.contact.id) ?? 0),
|
||||
false
|
||||
SessionUtil.SyncedContactInfo(
|
||||
id: data.contact.id,
|
||||
contact: data.contact,
|
||||
profile: data.profile,
|
||||
priority: Int32(allThreads[data.contact.id]?.pinnedPriority ?? 0),
|
||||
hidden: (allThreads[data.contact.id]?.shouldBeVisible == true)
|
||||
)
|
||||
},
|
||||
in: contactsConf
|
||||
)
|
||||
|
||||
if contactsConfResult.needsDump {
|
||||
if config_needs_dump(contactsConf) {
|
||||
try SessionUtil
|
||||
.createDump(
|
||||
conf: contactsConf,
|
||||
|
@ -130,15 +131,15 @@ enum _012_SharedUtilChanges: Migration {
|
|||
secretKey: secretKey,
|
||||
cachedData: nil
|
||||
)
|
||||
let convoInfoVolatileConfResult: SessionUtil.ConfResult = try SessionUtil.upsert(
|
||||
try SessionUtil.upsert(
|
||||
convoInfoVolatileChanges: volatileThreadInfo,
|
||||
in: convoInfoVolatileConf
|
||||
)
|
||||
|
||||
if convoInfoVolatileConfResult.needsDump {
|
||||
if config_needs_dump(convoInfoVolatileConf) {
|
||||
try SessionUtil
|
||||
.createDump(
|
||||
conf: contactsConf,
|
||||
conf: convoInfoVolatileConf,
|
||||
for: .convoInfoVolatile,
|
||||
publicKey: userPublicKey
|
||||
)?
|
||||
|
@ -155,16 +156,17 @@ enum _012_SharedUtilChanges: Migration {
|
|||
secretKey: secretKey,
|
||||
cachedData: nil
|
||||
)
|
||||
let userGroupConfResult1: SessionUtil.ConfResult = try SessionUtil.upsert(
|
||||
try SessionUtil.upsert(
|
||||
legacyGroups: legacyGroupData,
|
||||
in: userGroupsConf
|
||||
)
|
||||
let userGroupConfResult2: SessionUtil.ConfResult = try SessionUtil.upsert(
|
||||
communities: communityData.map { ($0, nil) },
|
||||
try SessionUtil.upsert(
|
||||
communities: communityData
|
||||
.map { SessionUtil.CommunityInfo(urlInfo: $0) },
|
||||
in: userGroupsConf
|
||||
)
|
||||
|
||||
if userGroupConfResult1.needsDump || userGroupConfResult2.needsDump {
|
||||
if config_needs_dump(userGroupsConf) {
|
||||
try SessionUtil
|
||||
.createDump(
|
||||
conf: userGroupsConf,
|
||||
|
@ -174,33 +176,18 @@ enum _012_SharedUtilChanges: Migration {
|
|||
.save(db)
|
||||
}
|
||||
|
||||
// MARK: - Pinned thread priorities
|
||||
// MARK: - Threads
|
||||
|
||||
struct PinnedTeadInfo: Decodable, FetchableRecord {
|
||||
let id: String
|
||||
let creationDateTimestamp: TimeInterval
|
||||
let maxInteractionTimestampMs: Int64?
|
||||
|
||||
var targetTimestamp: Int64 {
|
||||
(maxInteractionTimestampMs ?? Int64(creationDateTimestamp * 1000))
|
||||
}
|
||||
try SessionUtil
|
||||
.updatingThreads(db, Array(allThreads.values))
|
||||
|
||||
// MARK: - Syncing
|
||||
|
||||
// Enqueue a config sync job to ensure the generated configs get synced
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
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:**
|
||||
/// - The `variant` will be ignored if an existing thread is found
|
||||
/// - 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 {
|
||||
return try SessionThread(id: id, variant: variant)
|
||||
.saved(db)
|
||||
return try SessionThread(
|
||||
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 {
|
||||
|
@ -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 {
|
||||
struct PinnedPriority: TableRecord, ColumnExpressible {
|
||||
public typealias Columns = CodingKeys
|
||||
|
|
|
@ -17,6 +17,7 @@ public enum MessageReceiveJob: JobExecutor {
|
|||
deferred: @escaping (Job) -> ()
|
||||
) {
|
||||
guard
|
||||
let threadId: String = job.threadId,
|
||||
let detailsData: Data = job.details,
|
||||
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
|
||||
else {
|
||||
|
@ -59,10 +60,11 @@ public enum MessageReceiveJob: JobExecutor {
|
|||
do {
|
||||
try MessageReceiver.handle(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: messageInfo.threadVariant,
|
||||
message: messageInfo.message,
|
||||
serverExpirationTimestamp: messageInfo.serverExpirationTimestamp,
|
||||
associatedWithProto: protoContent,
|
||||
openGroupId: nil
|
||||
associatedWithProto: protoContent
|
||||
)
|
||||
}
|
||||
catch {
|
||||
|
@ -131,23 +133,27 @@ extension MessageReceiveJob {
|
|||
private enum CodingKeys: String, CodingKey {
|
||||
case message
|
||||
case variant
|
||||
case threadVariant
|
||||
case serverExpirationTimestamp
|
||||
case serializedProtoData
|
||||
}
|
||||
|
||||
public let message: Message
|
||||
public let variant: Message.Variant
|
||||
public let threadVariant: SessionThread.Variant
|
||||
public let serverExpirationTimestamp: TimeInterval?
|
||||
public let serializedProtoData: Data
|
||||
|
||||
public init(
|
||||
message: Message,
|
||||
variant: Message.Variant,
|
||||
threadVariant: SessionThread.Variant,
|
||||
serverExpirationTimestamp: TimeInterval?,
|
||||
proto: SNProtoContent
|
||||
) throws {
|
||||
self.message = message
|
||||
self.variant = variant
|
||||
self.threadVariant = threadVariant
|
||||
self.serverExpirationTimestamp = serverExpirationTimestamp
|
||||
self.serializedProtoData = try proto.serializedData()
|
||||
}
|
||||
|
@ -155,11 +161,13 @@ extension MessageReceiveJob {
|
|||
private init(
|
||||
message: Message,
|
||||
variant: Message.Variant,
|
||||
threadVariant: SessionThread.Variant,
|
||||
serverExpirationTimestamp: TimeInterval?,
|
||||
serializedProtoData: Data
|
||||
) {
|
||||
self.message = message
|
||||
self.variant = variant
|
||||
self.threadVariant = threadVariant
|
||||
self.serverExpirationTimestamp = serverExpirationTimestamp
|
||||
self.serializedProtoData = serializedProtoData
|
||||
}
|
||||
|
@ -177,6 +185,24 @@ extension MessageReceiveJob {
|
|||
self = MessageInfo(
|
||||
message: try variant.decode(from: container, forKey: .message),
|
||||
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),
|
||||
serializedProtoData: try container.decode(Data.self, forKey: .serializedProtoData)
|
||||
)
|
||||
|
@ -192,6 +218,7 @@ extension MessageReceiveJob {
|
|||
|
||||
try container.encode(message, forKey: .message)
|
||||
try container.encode(variant, forKey: .variant)
|
||||
try container.encode(threadVariant, forKey: .threadVariant)
|
||||
try container.encodeIfPresent(serverExpirationTimestamp, forKey: .serverExpirationTimestamp)
|
||||
try container.encode(serializedProtoData, forKey: .serializedProtoData)
|
||||
}
|
||||
|
|
|
@ -157,9 +157,6 @@ public enum MessageSendJob: JobExecutor {
|
|||
// Store the sentTimestamp from the message in case it fails due to a clockOutOfSync error
|
||||
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
|
||||
///
|
||||
/// **Note:** No need to upload attachments as part of this process as the above logic splits that out into it's own job
|
||||
|
|
|
@ -6,6 +6,16 @@ import SessionUtil
|
|||
import SessionUtilitiesKit
|
||||
|
||||
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
|
||||
|
||||
static func handleContactsUpdate(
|
||||
|
@ -56,6 +66,7 @@ internal extension SessionUtil {
|
|||
contactData[contactId] = (
|
||||
contactResult,
|
||||
profileResult,
|
||||
contact.hidden
|
||||
)
|
||||
contacts_iterator_advance(contactIterator)
|
||||
}
|
||||
|
@ -82,9 +93,9 @@ internal extension SessionUtil {
|
|||
|
||||
if
|
||||
(!data.profile.name.isEmpty && profile.name != data.profile.name) ||
|
||||
profile.nickname != data.profile.nickname ||
|
||||
profile.profilePictureUrl != data.profile.profilePictureUrl ||
|
||||
profile.profileEncryptionKey != data.profile.profileEncryptionKey
|
||||
profile.nickname != data.profile.nickname ||
|
||||
profile.profilePictureUrl != data.profile.profilePictureUrl ||
|
||||
profile.profileEncryptionKey != data.profile.profileEncryptionKey
|
||||
{
|
||||
try profile.save(db)
|
||||
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
|
||||
/// associated contact conversation accordingly
|
||||
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 {
|
||||
try SessionThread
|
||||
.deleteOne(db, id: contact.id)
|
||||
}
|
||||
else if !data.isHiddenConversation && !threadExists {
|
||||
try SessionThread(id: contact.id, variant: .contact)
|
||||
.save(db)
|
||||
switch (data.isHiddenConversation, threadExists, threadIsVisible) {
|
||||
case (true, true, _):
|
||||
try SessionThread
|
||||
.filter(id: contact.id)
|
||||
.deleteAll(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
|
||||
|
||||
typealias ContactData = (
|
||||
id: String,
|
||||
contact: Contact?,
|
||||
profile: Profile?,
|
||||
priority: Int32?,
|
||||
hidden: Bool?
|
||||
)
|
||||
|
||||
static func upsert(
|
||||
contactData: [ContactData],
|
||||
contactData: [SyncedContactInfo],
|
||||
in conf: UnsafeMutablePointer<config_object>?
|
||||
) throws -> ConfResult {
|
||||
) throws {
|
||||
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
||||
|
||||
// The current users contact data doesn't need to sync so exclude it
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey()
|
||||
let targetContacts: [SyncedContactInfo] = contactData
|
||||
.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
|
||||
guard !targetContacts.isEmpty else { return ConfResult(needsPush: false, needsDump: false) }
|
||||
guard !targetContacts.isEmpty else { return }
|
||||
|
||||
// Update the name
|
||||
targetContacts
|
||||
.forEach { (id, maybeContact, maybeProfile, priority, hidden) in
|
||||
var sessionId: [CChar] = id.cArray
|
||||
.forEach { info in
|
||||
var sessionId: [CChar] = info.id.cArray
|
||||
var contact: contacts_contact = contacts_contact()
|
||||
guard contacts_get_or_construct(conf, &contact, &sessionId) else {
|
||||
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)
|
||||
if let updatedContact: Contact = maybeContact {
|
||||
if let updatedContact: Contact = info.contact {
|
||||
contact.approved = updatedContact.isApproved
|
||||
contact.approved_me = updatedContact.didApproveMe
|
||||
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
|
||||
// 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 oldAvatarKey: Data? = Data(
|
||||
libSessionVal: contact.profile_pic.key,
|
||||
|
@ -209,9 +232,13 @@ internal extension SessionUtil {
|
|||
contact.profile_pic.url = updatedProfile.profilePictureUrl.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 {
|
||||
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)
|
||||
|
@ -219,15 +246,29 @@ internal extension SessionUtil {
|
|||
}
|
||||
|
||||
// Store the updated contact (can't be sure if we made any changes above)
|
||||
contact.hidden = (hidden ?? contact.hidden)
|
||||
contact.priority = (priority ?? contact.priority)
|
||||
contact.hidden = (info.hidden ?? contact.hidden)
|
||||
contact.priority = (info.priority ?? contact.priority)
|
||||
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 }
|
||||
|
||||
do {
|
||||
let atomicConf: Atomic<UnsafeMutablePointer<config_object>?> = SessionUtil.config(
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .contacts,
|
||||
publicKey: userPublicKey
|
||||
)
|
||||
|
||||
// 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
|
||||
let result: ConfResult = try SessionUtil
|
||||
) { conf in
|
||||
// When inserting new contacts (or contacts with invalid profile data) we want
|
||||
// to add any valid profile information we have so identify if any of the updated
|
||||
// contacts are new/invalid, and if so, fetch any profile data we have for them
|
||||
let newContactIds: [String] = targetContacts
|
||||
.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(
|
||||
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
|
||||
)
|
||||
|
||||
// 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 {
|
||||
|
@ -304,52 +360,30 @@ internal extension SessionUtil {
|
|||
do {
|
||||
// Update the user profile first (if needed)
|
||||
if let updatedUserProfile: Profile = updatedProfiles.first(where: { $0.id == userPublicKey }) {
|
||||
try SessionUtil
|
||||
.config(
|
||||
for: .userProfile,
|
||||
publicKey: userPublicKey
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userProfile,
|
||||
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
|
||||
// blocking access in it's `mutate` closure
|
||||
try SessionUtil
|
||||
.config(
|
||||
for: .contacts,
|
||||
publicKey: userPublicKey
|
||||
)
|
||||
.mutate { conf in
|
||||
let result: ConfResult = try SessionUtil
|
||||
.upsert(
|
||||
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)
|
||||
}
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .contacts,
|
||||
publicKey: userPublicKey
|
||||
) { conf in
|
||||
try SessionUtil
|
||||
.upsert(
|
||||
contactData: targetProfiles
|
||||
.map { SyncedContactInfo(id: $0.id, profile: $0) },
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
SNLog("[libSession-util] Failed to dump updated data")
|
||||
|
@ -358,3 +392,29 @@ internal extension SessionUtil {
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -158,16 +158,22 @@ internal extension SessionUtil {
|
|||
// If there are no newer local last read timestamps then just return the mergeResult
|
||||
guard !newerLocalChanges.isEmpty else { return }
|
||||
|
||||
try upsert(
|
||||
convoInfoVolatileChanges: newerLocalChanges,
|
||||
in: conf
|
||||
)
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .convoInfoVolatile,
|
||||
publicKey: getUserHexEncodedPublicKey(db)
|
||||
) { conf in
|
||||
try upsert(
|
||||
convoInfoVolatileChanges: newerLocalChanges,
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult static func upsert(
|
||||
static func upsert(
|
||||
convoInfoVolatileChanges: [VolatileThreadInfo],
|
||||
in conf: UnsafeMutablePointer<config_object>?
|
||||
) throws -> ConfResult {
|
||||
) throws {
|
||||
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
||||
|
||||
convoInfoVolatileChanges.forEach { threadInfo in
|
||||
|
@ -240,30 +246,23 @@ internal extension SessionUtil {
|
|||
}
|
||||
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
|
||||
|
||||
internal extension SessionUtil {
|
||||
@discardableResult static func updatingThreadsConvoInfoVolatile<T>(_ db: Database, _ updated: [T]) throws -> [T] {
|
||||
guard let updatedThreads: [SessionThread] = updated as? [SessionThread] else {
|
||||
throw StorageError.generic
|
||||
}
|
||||
|
||||
static func updateMarkedAsUnreadState(
|
||||
_ db: Database,
|
||||
threads: [SessionThread]
|
||||
) throws {
|
||||
// If we have no updated threads then no need to continue
|
||||
guard !updatedThreads.isEmpty else { return updated }
|
||||
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let changes: [VolatileThreadInfo] = try updatedThreads.map { thread in
|
||||
guard !threads.isEmpty else { return }
|
||||
|
||||
let changes: [VolatileThreadInfo] = try threads.map { thread in
|
||||
VolatileThreadInfo(
|
||||
threadId: thread.id,
|
||||
variant: thread.variant,
|
||||
|
@ -273,36 +272,17 @@ internal extension SessionUtil {
|
|||
changes: [.markedAsUnread(thread.markedAsUnread ?? false)]
|
||||
)
|
||||
}
|
||||
|
||||
do {
|
||||
try SessionUtil
|
||||
.config(
|
||||
for: .convoInfoVolatile,
|
||||
publicKey: userPublicKey
|
||||
)
|
||||
.mutate { conf in
|
||||
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .convoInfoVolatile,
|
||||
publicKey: getUserHexEncodedPublicKey(db)
|
||||
) { conf in
|
||||
try upsert(
|
||||
convoInfoVolatileChanges: changes,
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
catch {
|
||||
SNLog("[libSession-util] Failed to dump updated data")
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
static func syncThreadLastReadIfNeeded(
|
||||
|
@ -311,7 +291,6 @@ internal extension SessionUtil {
|
|||
threadVariant: SessionThread.Variant,
|
||||
lastReadTimestampMs: Int64
|
||||
) throws {
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let change: VolatileThreadInfo = VolatileThreadInfo(
|
||||
threadId: threadId,
|
||||
variant: threadVariant,
|
||||
|
@ -321,36 +300,15 @@ internal extension SessionUtil {
|
|||
changes: [.lastReadTimestampMs(lastReadTimestampMs)]
|
||||
)
|
||||
|
||||
let needsPush: Bool = try SessionUtil
|
||||
.config(
|
||||
for: .convoInfoVolatile,
|
||||
publicKey: userPublicKey
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .convoInfoVolatile,
|
||||
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,
|
||||
openGroup: OpenGroup?
|
||||
) -> Bool {
|
||||
let atomicConf: Atomic<UnsafeMutablePointer<config_object>?> = SessionUtil.config(
|
||||
for: .convoInfoVolatile,
|
||||
publicKey: userPublicKey
|
||||
)
|
||||
|
||||
// If we don't have a config then just assume it's unread
|
||||
guard atomicConf.wrappedValue != nil else { return false }
|
||||
|
||||
// Since we are doing direct memory manipulation we are using an `Atomic` type which has
|
||||
// blocking access in it's `mutate` closure
|
||||
return atomicConf.mutate { conf in
|
||||
switch threadVariant {
|
||||
case .contact:
|
||||
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)
|
||||
|
||||
case .legacyGroup:
|
||||
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
|
||||
}
|
||||
|
||||
return (legacyGroup.last_read > timestampMs)
|
||||
|
||||
case .community:
|
||||
guard let openGroup: OpenGroup = openGroup else { return false }
|
||||
|
||||
var cBaseUrl: [CChar] = openGroup.server.cArray
|
||||
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
|
||||
}
|
||||
|
||||
return (convoCommunity.last_read > timestampMs)
|
||||
|
||||
case .group: return false // TODO: Need to add when the type is added to the lib
|
||||
return SessionUtil
|
||||
.config(
|
||||
for: .convoInfoVolatile,
|
||||
publicKey: userPublicKey
|
||||
)
|
||||
.wrappedValue
|
||||
.map { conf in
|
||||
switch threadVariant {
|
||||
case .contact:
|
||||
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)
|
||||
|
||||
case .legacyGroup:
|
||||
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
|
||||
}
|
||||
|
||||
return (legacyGroup.last_read > timestampMs)
|
||||
|
||||
case .community:
|
||||
guard let openGroup: OpenGroup = openGroup else { return false }
|
||||
|
||||
var cBaseUrl: [CChar] = openGroup.server.cArray
|
||||
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
|
||||
}
|
||||
|
||||
return (convoCommunity.last_read > timestampMs)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,143 +8,172 @@ import SessionUtilitiesKit
|
|||
// MARK: - Convenience
|
||||
|
||||
internal extension SessionUtil {
|
||||
static let columnsRelatedToThreads: [ColumnExpression] = [
|
||||
SessionThread.Columns.pinnedPriority,
|
||||
SessionThread.Columns.shouldBeVisible
|
||||
]
|
||||
|
||||
static func assignmentsRequireConfigUpdate(_ assignments: [ConfigColumnAssignment]) -> Bool {
|
||||
let targetColumns: Set<ColumnKey> = Set(assignments.map { ColumnKey($0.column) })
|
||||
let allColumnsThatTriggerConfigUpdate: Set<ColumnKey> = []
|
||||
.appending(contentsOf: columnsRelatedToUserProfile)
|
||||
.appending(contentsOf: columnsRelatedToContacts)
|
||||
.appending(contentsOf: columnsRelatedToConvoInfoVolatile)
|
||||
.appending(contentsOf: columnsRelatedToUserGroups)
|
||||
.map { ColumnKey($0) }
|
||||
.asSet()
|
||||
|
||||
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
|
||||
/// priorities get updated in the HomeVC
|
||||
static func updateThreadPrioritiesIfNeeded<T>(
|
||||
|
||||
static func performAndPushChange(
|
||||
_ db: Database,
|
||||
_ assignments: [ConfigColumnAssignment],
|
||||
_ updated: [T]
|
||||
for variant: ConfigDump.Variant,
|
||||
publicKey: String,
|
||||
change: (UnsafeMutablePointer<config_object>?) throws -> ()
|
||||
) throws {
|
||||
// Note: This logic 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 priorities get updated
|
||||
// in the HomeVC
|
||||
// Since we are doing direct memory manipulation we are using an `Atomic`
|
||||
// type which has blocking access in it's `mutate` closure
|
||||
let needsPush: Bool = try SessionUtil
|
||||
.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 pinnedThreadInfo: [PriorityInfo] = try SessionThread
|
||||
.select(.id, .variant, .pinnedPriority)
|
||||
.asRequest(of: PriorityInfo.self)
|
||||
.fetchAll(db)
|
||||
let groupedPriorityInfo: [SessionThread.Variant: [PriorityInfo]] = pinnedThreadInfo
|
||||
let groupedThreads: [SessionThread.Variant: [SessionThread]] = updatedThreads
|
||||
.grouped(by: \.variant)
|
||||
let pinnedCommunities: [String: OpenGroupUrlInfo] = try OpenGroupUrlInfo
|
||||
.fetchAll(db, ids: pinnedThreadInfo.map { $0.id })
|
||||
let urlInfo: [String: OpenGroupUrlInfo] = try OpenGroupUrlInfo
|
||||
.fetchAll(db, ids: updatedThreads.map { $0.id })
|
||||
.reduce(into: [:]) { result, next in result[next.threadId] = next }
|
||||
|
||||
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 {
|
||||
case .contact:
|
||||
// If the 'Note to Self' conversation is pinned then we need to custom handle it
|
||||
// first as it's part of the UserProfile config
|
||||
if let noteToSelfPriority: PriorityInfo = priorityInfo.first(where: { $0.id == userPublicKey }) {
|
||||
let atomicConf: Atomic<UnsafeMutablePointer<config_object>?> = SessionUtil.config(
|
||||
if let noteToSelf: SessionThread = threads.first(where: { $0.id == userPublicKey }) {
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userProfile,
|
||||
publicKey: userPublicKey
|
||||
)
|
||||
|
||||
try SessionUtil.updateNoteToSelfPriority(
|
||||
db,
|
||||
priority: Int32(noteToSelfPriority.pinnedPriority ?? 0),
|
||||
in: atomicConf
|
||||
)
|
||||
) { conf in
|
||||
try SessionUtil.updateNoteToSelf(
|
||||
db,
|
||||
priority: noteToSelf.pinnedPriority
|
||||
.map { Int32($0 == 0 ? 0 : max($0, 1)) }
|
||||
.defaulting(to: 0),
|
||||
hidden: noteToSelf.shouldBeVisible,
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 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`
|
||||
// type which has blocking access in it's `mutate` closure
|
||||
try SessionUtil
|
||||
.config(
|
||||
for: .contacts,
|
||||
publicKey: userPublicKey
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .contacts,
|
||||
publicKey: userPublicKey
|
||||
) { conf in
|
||||
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:
|
||||
// Since we are doing direct memory manipulation we are using an `Atomic`
|
||||
// type which has blocking access in it's `mutate` closure
|
||||
try SessionUtil
|
||||
.config(
|
||||
for: .userGroups,
|
||||
publicKey: userPublicKey
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
publicKey: userPublicKey
|
||||
) { conf in
|
||||
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:
|
||||
// Since we are doing direct memory manipulation we are using an `Atomic`
|
||||
// type which has blocking access in it's `mutate` closure
|
||||
try SessionUtil
|
||||
.config(
|
||||
for: .userGroups,
|
||||
publicKey: userPublicKey
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
publicKey: userPublicKey
|
||||
) { conf in
|
||||
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:
|
||||
// TODO: Add this
|
||||
|
@ -155,6 +184,8 @@ internal extension SessionUtil {
|
|||
catch {
|
||||
SNLog("[libSession-util] Failed to dump updated data")
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -184,12 +215,13 @@ internal extension SessionUtil {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Pinned Priority
|
||||
// MARK: - PriorityVisibilityInfo
|
||||
|
||||
extension SessionUtil {
|
||||
struct PriorityInfo: Codable, FetchableRecord, Identifiable {
|
||||
struct PriorityVisibilityInfo: Codable, FetchableRecord, Identifiable {
|
||||
let id: String
|
||||
let variant: SessionThread.Variant
|
||||
let pinnedPriority: Int32?
|
||||
let shouldBeVisible: Bool
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,12 @@ import SessionUtilitiesKit
|
|||
import SessionSnodeKit
|
||||
// TODO: Expose 'GROUP_NAME_MAX_LENGTH', 'COMMUNITY_URL_MAX_LENGTH' & 'COMMUNITY_ROOM_MAX_LENGTH'
|
||||
internal extension SessionUtil {
|
||||
static let columnsRelatedToUserGroups: [ColumnExpression] = [
|
||||
ClosedGroup.Columns.name
|
||||
]
|
||||
|
||||
// MARK: - Incoming Changes
|
||||
|
||||
|
||||
static func handleGroupsUpdate(
|
||||
_ db: Database,
|
||||
in conf: UnsafeMutablePointer<config_object>?,
|
||||
|
@ -20,12 +24,12 @@ internal extension SessionUtil {
|
|||
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
||||
|
||||
var communities: [PrioritisedData<OpenGroupUrlInfo>] = []
|
||||
var legacyGroups: [PrioritisedData<LegacyGroupInfo>] = []
|
||||
var legacyGroups: [LegacyGroupInfo] = []
|
||||
var groups: [PrioritisedData<String>] = []
|
||||
var community: ugroups_community_info = ugroups_community_info()
|
||||
var legacyGroup: ugroups_legacy_group_info = ugroups_legacy_group_info()
|
||||
let groupsIterator: OpaquePointer = user_groups_iterator_new(conf)
|
||||
|
||||
|
||||
while !user_groups_iterator_done(groupsIterator) {
|
||||
if user_groups_it_is_community(groupsIterator, &community) {
|
||||
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) {
|
||||
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(
|
||||
PrioritisedData(
|
||||
data: LegacyGroupInfo(
|
||||
id: groupId,
|
||||
name: String(libSessionVal: legacyGroup.name),
|
||||
lastKeyPair: ClosedGroupKeyPair(
|
||||
threadId: groupId,
|
||||
publicKey: Data(
|
||||
libSessionVal: legacyGroup.enc_pubkey,
|
||||
count: ClosedGroup.pubkeyByteLength
|
||||
),
|
||||
secretKey: Data(
|
||||
libSessionVal: legacyGroup.enc_seckey,
|
||||
count: ClosedGroup.secretKeyByteLength
|
||||
),
|
||||
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
LegacyGroupInfo(
|
||||
id: groupId,
|
||||
name: String(libSessionVal: legacyGroup.name),
|
||||
lastKeyPair: ClosedGroupKeyPair(
|
||||
threadId: groupId,
|
||||
publicKey: Data(
|
||||
libSessionVal: legacyGroup.enc_pubkey,
|
||||
count: ClosedGroup.pubkeyByteLength
|
||||
),
|
||||
disappearingConfig: DisappearingMessagesConfiguration
|
||||
.defaultWith(groupId)
|
||||
.with(
|
||||
// TODO: double check the 'isEnabled' flag
|
||||
isEnabled: (legacyGroup.disappearing_timer > 0),
|
||||
durationSeconds: (legacyGroup.disappearing_timer == 0 ? nil :
|
||||
TimeInterval(legacyGroup.disappearing_timer)
|
||||
)
|
||||
),
|
||||
groupMembers: [], //[GroupMember] // TODO: This
|
||||
hidden: legacyGroup.hidden
|
||||
secretKey: Data(
|
||||
libSessionVal: legacyGroup.enc_seckey,
|
||||
count: ClosedGroup.secretKeyByteLength
|
||||
),
|
||||
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
),
|
||||
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
|
||||
)
|
||||
)
|
||||
|
@ -89,13 +109,13 @@ internal extension SessionUtil {
|
|||
user_groups_iterator_advance(groupsIterator)
|
||||
}
|
||||
user_groups_iterator_free(groupsIterator) // Need to free the iterator
|
||||
|
||||
|
||||
// If we don't have any conversations then no need to continue
|
||||
guard !communities.isEmpty || !legacyGroups.isEmpty || !groups.isEmpty else { return }
|
||||
|
||||
// Extract all community/legacyGroup/group thread priorities
|
||||
let existingThreadPriorities: [String: PriorityInfo] = (try? SessionThread
|
||||
.select(.id, .variant, .pinnedPriority)
|
||||
let existingThreadInfo: [String: PriorityVisibilityInfo] = (try? SessionThread
|
||||
.select(.id, .variant, .pinnedPriority, .shouldBeVisible)
|
||||
.filter(
|
||||
[
|
||||
SessionThread.Variant.community,
|
||||
|
@ -103,7 +123,7 @@ internal extension SessionUtil {
|
|||
SessionThread.Variant.group
|
||||
].contains(SessionThread.Columns.variant)
|
||||
)
|
||||
.asRequest(of: PriorityInfo.self)
|
||||
.asRequest(of: PriorityVisibilityInfo.self)
|
||||
.fetchAll(db))
|
||||
.defaulting(to: [])
|
||||
.reduce(into: [:]) { result, next in result[next.id] = next }
|
||||
|
@ -121,10 +141,10 @@ internal extension SessionUtil {
|
|||
calledFromConfigHandling: true
|
||||
)
|
||||
.sinkUntilComplete()
|
||||
|
||||
|
||||
// Set the priority if it's changed (new communities will have already been inserted at
|
||||
// this stage)
|
||||
if existingThreadPriorities[community.data.threadId]?.pinnedPriority != community.priority {
|
||||
if existingThreadInfo[community.data.threadId]?.pinnedPriority != community.priority {
|
||||
_ = try? SessionThread
|
||||
.filter(id: community.data.threadId)
|
||||
.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
|
||||
let communityIdsToRemove: Set<String> = Set(existingThreadPriorities
|
||||
let communityIdsToRemove: Set<String> = Set(existingThreadInfo
|
||||
.filter { $0.value.variant == .community }
|
||||
.keys)
|
||||
.subtracting(communities.map { $0.data.threadId })
|
||||
|
@ -150,22 +170,31 @@ internal extension SessionUtil {
|
|||
|
||||
// MARK: -- Handle Legacy Group Changes
|
||||
|
||||
let existingLegacyGroupIds: Set<String> = Set(existingThreadPriorities
|
||||
let existingLegacyGroupIds: Set<String> = Set(existingThreadInfo
|
||||
.filter { $0.value.variant == .legacyGroup }
|
||||
.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
|
||||
guard
|
||||
let name: String = group.data.name,
|
||||
let lastKeyPair: ClosedGroupKeyPair = group.data.lastKeyPair,
|
||||
let members: [GroupMember] = group.data.groupMembers
|
||||
let name: String = group.name,
|
||||
let lastKeyPair: ClosedGroupKeyPair = group.lastKeyPair,
|
||||
let members: [GroupMember] = group.groupMembers
|
||||
else { return }
|
||||
|
||||
if !existingLegacyGroupIds.contains(group.data.id) {
|
||||
if !existingLegacyGroupIds.contains(group.id) {
|
||||
// Add a new group if it doesn't already exist
|
||||
try MessageReceiver.handleNewClosedGroup(
|
||||
db,
|
||||
groupPublicKey: group.data.id,
|
||||
groupPublicKey: group.id,
|
||||
name: name,
|
||||
encryptionKeyPair: Box.KeyPair(
|
||||
publicKey: lastKeyPair.publicKey.bytes,
|
||||
|
@ -177,20 +206,22 @@ internal extension SessionUtil {
|
|||
admins: members
|
||||
.filter { $0.role == .admin }
|
||||
.map { $0.profileId },
|
||||
expirationTimer: UInt32(group.data.disappearingConfig?.durationSeconds ?? 0),
|
||||
expirationTimer: UInt32(group.disappearingConfig?.durationSeconds ?? 0),
|
||||
messageSentTimestamp: UInt64(latestConfigUpdateSentTimestamp * 1000)
|
||||
)
|
||||
}
|
||||
else {
|
||||
// Otherwise update the existing group
|
||||
_ = try? ClosedGroup
|
||||
.filter(id: group.data.id)
|
||||
.updateAll( // Handling a config update so don't use `updateAllAndConfig`
|
||||
db,
|
||||
ClosedGroup.Columns.name.set(to: name)
|
||||
)
|
||||
if existingLegacyGroups[group.id]?.name != name {
|
||||
_ = try? ClosedGroup
|
||||
.filter(id: group.id)
|
||||
.updateAll( // Handling a config update so don't use `updateAllAndConfig`
|
||||
db,
|
||||
ClosedGroup.Columns.name.set(to: name)
|
||||
)
|
||||
}
|
||||
|
||||
// Update the lastKey
|
||||
// Add the lastKey if it doesn't already exist
|
||||
let keyPairExists: Bool = ClosedGroupKeyPair
|
||||
.filter(
|
||||
ClosedGroupKeyPair.Columns.threadId == lastKeyPair.threadId &&
|
||||
|
@ -205,12 +236,11 @@ internal extension SessionUtil {
|
|||
|
||||
// Update the disappearing messages timer
|
||||
_ = try DisappearingMessagesConfiguration
|
||||
.fetchOne(db, id: group.data.id)
|
||||
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(group.data.id))
|
||||
.fetchOne(db, id: group.id)
|
||||
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(group.id))
|
||||
.with(
|
||||
// TODO: double check the 'isEnabled' flag
|
||||
isEnabled: (group.data.disappearingConfig?.isEnabled == true),
|
||||
durationSeconds: group.data.disappearingConfig?.durationSeconds
|
||||
isEnabled: (group.disappearingConfig?.isEnabled == true),
|
||||
durationSeconds: group.disappearingConfig?.durationSeconds
|
||||
)
|
||||
.saved(db)
|
||||
|
||||
|
@ -221,23 +251,35 @@ internal extension SessionUtil {
|
|||
// 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
|
||||
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
|
||||
.filter(id: group.data.id)
|
||||
.filter(id: group.id)
|
||||
.updateAll( // Handling a config update so don't use `updateAllAndConfig`
|
||||
db,
|
||||
SessionThread.Columns.pinnedPriority.set(to: group.priority)
|
||||
threadChanges
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any legacy groups which are no longer in the config
|
||||
let legacyGroupIdsToRemove: Set<String> = existingLegacyGroupIds
|
||||
.subtracting(legacyGroups.map { $0.data.id })
|
||||
.subtracting(legacyGroups.map { $0.id })
|
||||
|
||||
if !legacyGroupIdsToRemove.isEmpty {
|
||||
try ClosedGroup.removeKeysAndUnsubscribe(
|
||||
|
@ -251,16 +293,16 @@ internal extension SessionUtil {
|
|||
// MARK: -- Handle Group Changes
|
||||
// TODO: Add this
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Outgoing Changes
|
||||
|
||||
static func upsert(
|
||||
legacyGroups: [LegacyGroupInfo],
|
||||
in conf: UnsafeMutablePointer<config_object>?
|
||||
) throws -> ConfResult {
|
||||
) throws {
|
||||
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
|
||||
// blocking access in it's `mutate` closure
|
||||
legacyGroups
|
||||
|
@ -268,13 +310,12 @@ internal extension SessionUtil {
|
|||
var cGroupId: [CChar] = legacyGroup.id.cArray
|
||||
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)
|
||||
if let updatedName: String = legacyGroup.name {
|
||||
userGroup.pointee.name = updatedName.toLibSession()
|
||||
|
||||
// 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 {
|
||||
|
@ -282,7 +323,7 @@ internal extension SessionUtil {
|
|||
userGroup.pointee.enc_seckey = lastKeyPair.secretKey.toLibSession()
|
||||
|
||||
// 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)
|
||||
|
@ -291,10 +332,21 @@ internal extension SessionUtil {
|
|||
userGroup.pointee.disappearing_timer = (!updatedConfig.isEnabled ? 0 :
|
||||
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)
|
||||
userGroup.pointee.hidden = (legacyGroup.hidden ?? userGroup.pointee.hidden)
|
||||
userGroup.pointee.priority = (legacyGroup.priority ?? userGroup.pointee.priority)
|
||||
|
@ -302,25 +354,20 @@ internal extension SessionUtil {
|
|||
// Note: Need to free the legacy group pointer
|
||||
user_groups_set_free_legacy_group(conf, userGroup)
|
||||
}
|
||||
|
||||
return ConfResult(
|
||||
needsPush: config_needs_push(conf),
|
||||
needsDump: config_needs_dump(conf)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
static func upsert(
|
||||
communities: [(info: OpenGroupUrlInfo, priority: Int32?)],
|
||||
communities: [CommunityInfo],
|
||||
in conf: UnsafeMutablePointer<config_object>?
|
||||
) throws -> ConfResult {
|
||||
) throws {
|
||||
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
||||
guard !communities.isEmpty else { return ConfResult.init(needsPush: false, needsDump: false) }
|
||||
|
||||
guard !communities.isEmpty else { return }
|
||||
|
||||
communities
|
||||
.forEach { info, priority in
|
||||
var cBaseUrl: [CChar] = info.server.cArray
|
||||
var cRoom: [CChar] = info.roomToken.cArray
|
||||
var cPubkey: [UInt8] = Data(hex: info.publicKey).cArray
|
||||
.forEach { community in
|
||||
var cBaseUrl: [CChar] = community.urlInfo.server.cArray
|
||||
var cRoom: [CChar] = community.urlInfo.roomToken.cArray
|
||||
var cPubkey: [UInt8] = Data(hex: community.urlInfo.publicKey).cArray
|
||||
var userCommunity: ugroups_community_info = ugroups_community_info()
|
||||
|
||||
guard user_groups_get_or_construct_community(conf, &userCommunity, &cBaseUrl, &cRoom, &cPubkey) else {
|
||||
|
@ -328,15 +375,15 @@ internal extension SessionUtil {
|
|||
return
|
||||
}
|
||||
|
||||
userCommunity.priority = (priority ?? userCommunity.priority)
|
||||
userCommunity.priority = (community.priority ?? userCommunity.priority)
|
||||
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
|
||||
|
||||
|
@ -346,91 +393,38 @@ internal extension SessionUtil {
|
|||
rootToken: String,
|
||||
publicKey: String
|
||||
) throws {
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
// Since we are doing direct memory manipulation we are using an `Atomic` type which has
|
||||
// blocking access in it's `mutate` closure
|
||||
let needsPush: Bool = try SessionUtil
|
||||
.config(
|
||||
for: .userGroups,
|
||||
publicKey: userPublicKey
|
||||
)
|
||||
.mutate { conf in
|
||||
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
||||
|
||||
let result: ConfResult = try SessionUtil.upsert(
|
||||
communities: [
|
||||
(
|
||||
OpenGroupUrlInfo(
|
||||
threadId: OpenGroup.idFor(roomToken: rootToken, server: server),
|
||||
server: server,
|
||||
roomToken: rootToken,
|
||||
publicKey: publicKey
|
||||
),
|
||||
nil
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
publicKey: getUserHexEncodedPublicKey(db)
|
||||
) { conf in
|
||||
try SessionUtil.upsert(
|
||||
communities: [
|
||||
CommunityInfo(
|
||||
urlInfo: OpenGroupUrlInfo(
|
||||
threadId: OpenGroup.idFor(roomToken: rootToken, server: server),
|
||||
server: server,
|
||||
roomToken: rootToken,
|
||||
publicKey: publicKey
|
||||
)
|
||||
],
|
||||
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)
|
||||
)
|
||||
],
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static func remove(_ db: Database, server: String, roomToken: String) throws {
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
// Since we are doing direct memory manipulation we are using an `Atomic` type which has
|
||||
// blocking access in it's `mutate` closure
|
||||
let needsPush: Bool = try SessionUtil
|
||||
.config(
|
||||
for: .userGroups,
|
||||
publicKey: userPublicKey
|
||||
)
|
||||
.mutate { conf in
|
||||
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)
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
publicKey: getUserHexEncodedPublicKey(db)
|
||||
) { conf in
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -446,116 +440,121 @@ internal extension SessionUtil {
|
|||
members: Set<String>,
|
||||
admins: Set<String>
|
||||
) throws {
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
// Since we are doing direct memory manipulation we are using an `Atomic` type which has
|
||||
// blocking access in it's `mutate` closure
|
||||
let needsPush: Bool = try SessionUtil
|
||||
.config(
|
||||
for: .userGroups,
|
||||
publicKey: userPublicKey
|
||||
)
|
||||
.mutate { conf in
|
||||
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
||||
|
||||
let result: ConfResult = try SessionUtil.upsert(
|
||||
legacyGroups: [
|
||||
LegacyGroupInfo(
|
||||
id: groupPublicKey,
|
||||
name: name,
|
||||
lastKeyPair: ClosedGroupKeyPair(
|
||||
threadId: groupPublicKey,
|
||||
publicKey: latestKeyPairPublicKey,
|
||||
secretKey: latestKeyPairSecretKey,
|
||||
receivedTimestamp: latestKeyPairReceivedTimestamp
|
||||
),
|
||||
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
|
||||
)
|
||||
}
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
publicKey: getUserHexEncodedPublicKey(db)
|
||||
) { conf in
|
||||
try SessionUtil.upsert(
|
||||
legacyGroups: [
|
||||
LegacyGroupInfo(
|
||||
id: groupPublicKey,
|
||||
name: name,
|
||||
lastKeyPair: ClosedGroupKeyPair(
|
||||
threadId: groupPublicKey,
|
||||
publicKey: latestKeyPairPublicKey,
|
||||
secretKey: latestKeyPairSecretKey,
|
||||
receivedTimestamp: latestKeyPairReceivedTimestamp
|
||||
),
|
||||
groupMembers: members
|
||||
.map { memberId in
|
||||
GroupMember(
|
||||
groupId: groupPublicKey,
|
||||
profileId: memberId,
|
||||
role: .standard,
|
||||
isHidden: false
|
||||
)
|
||||
)
|
||||
],
|
||||
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)
|
||||
},
|
||||
groupAdmins: admins
|
||||
.map { memberId in
|
||||
GroupMember(
|
||||
groupId: groupPublicKey,
|
||||
profileId: memberId,
|
||||
role: .admin,
|
||||
isHidden: false
|
||||
)
|
||||
}
|
||||
)
|
||||
],
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static func update(
|
||||
_ db: Database,
|
||||
groupPublicKey: String,
|
||||
name: String? = nil,
|
||||
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 {
|
||||
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 {
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
// Since we are doing direct memory manipulation we are using an `Atomic` type which has
|
||||
// blocking access in it's `mutate` closure
|
||||
let needsPush: Bool = try SessionUtil
|
||||
.config(
|
||||
for: .userGroups,
|
||||
publicKey: userPublicKey
|
||||
)
|
||||
.mutate { conf in
|
||||
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
publicKey: getUserHexEncodedPublicKey(db)
|
||||
) { conf in
|
||||
legacyGroupIds.forEach { threadId in
|
||||
var cGroupId: [CChar] = threadId.cArray
|
||||
|
||||
legacyGroupIds.forEach { threadId in
|
||||
var cGroupId: [CChar] = threadId.cArray
|
||||
|
||||
// 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
|
||||
// Don't care if the group doesn't exist
|
||||
user_groups_erase_legacy_group(conf, &cGroupId)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
extension SessionUtil {
|
||||
|
@ -585,6 +578,7 @@ extension SessionUtil {
|
|||
case lastKeyPair
|
||||
case disappearingConfig
|
||||
case groupMembers
|
||||
case groupAdmins
|
||||
case hidden
|
||||
case priority
|
||||
}
|
||||
|
@ -596,6 +590,7 @@ extension SessionUtil {
|
|||
let lastKeyPair: ClosedGroupKeyPair?
|
||||
let disappearingConfig: DisappearingMessagesConfiguration?
|
||||
let groupMembers: [GroupMember]?
|
||||
let groupAdmins: [GroupMember]?
|
||||
let hidden: Bool?
|
||||
let priority: Int32?
|
||||
|
||||
|
@ -605,6 +600,7 @@ extension SessionUtil {
|
|||
lastKeyPair: ClosedGroupKeyPair? = nil,
|
||||
disappearingConfig: DisappearingMessagesConfiguration? = nil,
|
||||
groupMembers: [GroupMember]? = nil,
|
||||
groupAdmins: [GroupMember]? = nil,
|
||||
hidden: Bool? = nil,
|
||||
priority: Int32? = nil
|
||||
) {
|
||||
|
@ -613,6 +609,7 @@ extension SessionUtil {
|
|||
self.lastKeyPair = lastKeyPair
|
||||
self.disappearingConfig = disappearingConfig
|
||||
self.groupMembers = groupMembers
|
||||
self.groupAdmins = groupAdmins
|
||||
self.hidden = hidden
|
||||
self.priority = priority
|
||||
}
|
||||
|
@ -625,7 +622,17 @@ extension SessionUtil {
|
|||
.order(ClosedGroupKeyPair.Columns.receivedTimestamp.desc)
|
||||
.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(
|
||||
optional: ClosedGroup.thread
|
||||
.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 {
|
||||
let communities: [PrioritisedData<SessionUtil.OpenGroupUrlInfo>]
|
||||
let legacyGroups: [PrioritisedData<LegacyGroupInfo>]
|
||||
|
|
|
@ -76,7 +76,7 @@ internal extension SessionUtil {
|
|||
static func update(
|
||||
profile: Profile,
|
||||
in conf: UnsafeMutablePointer<config_object>?
|
||||
) throws -> ConfResult {
|
||||
) throws {
|
||||
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
||||
|
||||
// Update the name
|
||||
|
@ -88,35 +88,17 @@ internal extension SessionUtil {
|
|||
profilePic.url = profile.profilePictureUrl.toLibSession()
|
||||
profilePic.key = profile.profileEncryptionKey.toLibSession()
|
||||
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,
|
||||
priority: Int32,
|
||||
in atomicConf: Atomic<UnsafeMutablePointer<config_object>?>
|
||||
hidden: Bool,
|
||||
in conf: UnsafeMutablePointer<config_object>?
|
||||
) throws {
|
||||
guard atomicConf.wrappedValue != nil else { throw SessionUtilError.nilConfigObject }
|
||||
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
||||
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
// 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)
|
||||
}
|
||||
user_profile_set_nts_priority(conf, priority)
|
||||
user_profile_set_nts_hidden(conf, hidden)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -130,9 +130,11 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table
|
|||
case is QueryInterfaceRequest<Profile>:
|
||||
return try SessionUtil.updatingProfiles(db, updatedData)
|
||||
|
||||
case is QueryInterfaceRequest<SessionThread>:
|
||||
case is QueryInterfaceRequest<ClosedGroup>:
|
||||
return updatedData
|
||||
|
||||
|
||||
case is QueryInterfaceRequest<SessionThread>:
|
||||
return try SessionUtil.updatingThreads(db, updatedData)
|
||||
|
||||
default: return updatedData
|
||||
}
|
||||
|
|
|
@ -4,6 +4,18 @@
|
|||
<dict>
|
||||
<key>AvailableLibraries</key>
|
||||
<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>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64_x86_64-simulator</string>
|
||||
|
@ -19,18 +31,6 @@
|
|||
<key>SupportedPlatformVariant</key>
|
||||
<string>simulator</string>
|
||||
</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>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XFWK</string>
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -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
|
||||
/// freed (via `free()`) when done with it.
|
||||
typedef struct config_string_list {
|
||||
char** value; // array of null-terminated C strings
|
||||
size_t len; // length of `value`
|
||||
char** value; // array of null-terminated C strings
|
||||
size_t len; // length of `value`
|
||||
} config_string_list;
|
||||
|
||||
/// Obtains the current active hashes. Note that this will be empty if the current hash is unknown
|
||||
|
|
|
@ -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
|
||||
// where you don't care at all about the pubkey).
|
||||
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
|
||||
// (fixed-length 32 byte buffer). The full URL is written to `full_url`, which must be at least
|
||||
|
|
|
@ -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).
|
||||
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
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
|
|
@ -16,6 +16,8 @@ namespace session::config {
|
|||
/// q - user profile decryption key (binary)
|
||||
/// + - the priority value for the "Note to Self" pseudo-conversation (higher = higher in the
|
||||
/// 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 {
|
||||
|
||||
|
@ -55,11 +57,18 @@ class UserProfile final : public ConfigBase {
|
|||
void set_profile_pic(std::string_view url, ustring_view key);
|
||||
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;
|
||||
|
||||
/// Sets the Note-to-self conversation priority. Should be >= 0 (negatives will be set to 0).
|
||||
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
|
||||
|
|
|
@ -8,12 +8,10 @@ import SessionUtilitiesKit
|
|||
/// Abstract base class for `VisibleMessage` and `ControlMessage`.
|
||||
public class Message: Codable {
|
||||
public var id: String?
|
||||
public var threadId: String?
|
||||
public var sentTimestamp: UInt64?
|
||||
public var receivedTimestamp: UInt64?
|
||||
public var recipient: String?
|
||||
public var sender: String?
|
||||
public var groupPublicKey: String?
|
||||
public var openGroupServerMessageId: UInt64?
|
||||
public var serverHash: String?
|
||||
|
||||
|
@ -34,7 +32,6 @@ public class Message: Codable {
|
|||
|
||||
public init(
|
||||
id: String? = nil,
|
||||
threadId: String? = nil,
|
||||
sentTimestamp: UInt64? = nil,
|
||||
receivedTimestamp: UInt64? = nil,
|
||||
recipient: String? = nil,
|
||||
|
@ -44,12 +41,10 @@ public class Message: Codable {
|
|||
serverHash: String? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.threadId = threadId
|
||||
self.sentTimestamp = sentTimestamp
|
||||
self.receivedTimestamp = receivedTimestamp
|
||||
self.recipient = recipient
|
||||
self.sender = sender
|
||||
self.groupPublicKey = groupPublicKey
|
||||
self.openGroupServerMessageId = openGroupServerMessageId
|
||||
self.serverHash = serverHash
|
||||
}
|
||||
|
@ -68,14 +63,13 @@ public class Message: Codable {
|
|||
// MARK: - Message Parsing/Processing
|
||||
|
||||
public typealias ProcessedMessage = (
|
||||
threadId: String?,
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
proto: SNProtoContent,
|
||||
messageInfo: MessageReceiveJob.Details.MessageInfo
|
||||
)
|
||||
|
||||
public extension Message {
|
||||
static let nonThreadMessageId: String = "NON_THREAD_MESSAGE"
|
||||
|
||||
enum Variant: String, Codable {
|
||||
case readReceipt
|
||||
case typingIndicator
|
||||
|
@ -485,7 +479,7 @@ public extension Message {
|
|||
handleClosedGroupKeyUpdateMessages: Bool,
|
||||
dependencies: SMKDependencies = SMKDependencies()
|
||||
) throws -> ProcessedMessage? {
|
||||
let (message, proto, threadId) = try MessageReceiver.parse(
|
||||
let (message, proto, threadId, threadVariant) = try MessageReceiver.parse(
|
||||
db,
|
||||
envelope: envelope,
|
||||
serverExpirationTimestamp: serverExpirationTimestamp,
|
||||
|
@ -511,7 +505,12 @@ public extension Message {
|
|||
case let closedGroupControlMessage as ClosedGroupControlMessage:
|
||||
switch closedGroupControlMessage.kind {
|
||||
case .encryptionKeyPair:
|
||||
try MessageReceiver.handleClosedGroupControlMessage(db, closedGroupControlMessage)
|
||||
try MessageReceiver.handleClosedGroupControlMessage(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
message: closedGroupControlMessage
|
||||
)
|
||||
return nil
|
||||
|
||||
default: break
|
||||
|
@ -540,10 +539,12 @@ public extension Message {
|
|||
|
||||
return (
|
||||
threadId,
|
||||
threadVariant,
|
||||
proto,
|
||||
try MessageReceiveJob.Details.MessageInfo(
|
||||
message: message,
|
||||
variant: variant,
|
||||
threadVariant: threadVariant,
|
||||
serverExpirationTimestamp: serverExpirationTimestamp,
|
||||
proto: proto
|
||||
)
|
||||
|
|
|
@ -229,17 +229,19 @@ public final class OpenGroupManager {
|
|||
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
|
||||
// inactive one but that won't matter as we then activate it
|
||||
_ = try? SessionThread.fetchOrCreate(db, id: threadId, variant: .community)
|
||||
|
||||
// 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 we want to wait until it actually has messages before making
|
||||
// it visible)
|
||||
if !calledFromConfigHandling {
|
||||
_ = try? SessionThread
|
||||
.filter(id: threadId)
|
||||
.updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||
}
|
||||
// inactive one but that won't matter as we then activate it)
|
||||
_ = try? SessionThread
|
||||
.fetchOrCreate(
|
||||
db,
|
||||
id: threadId,
|
||||
variant: .community,
|
||||
/// 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
|
||||
/// we want to wait until it actually has messages before making it visible)
|
||||
///
|
||||
/// **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 {
|
||||
try? OpenGroup
|
||||
|
@ -641,10 +643,11 @@ public final class OpenGroupManager {
|
|||
if let messageInfo: MessageReceiveJob.Details.MessageInfo = processedMessage?.messageInfo {
|
||||
try MessageReceiver.handle(
|
||||
db,
|
||||
threadId: openGroup.id,
|
||||
threadVariant: .community,
|
||||
message: messageInfo.message,
|
||||
serverExpirationTimestamp: messageInfo.serverExpirationTimestamp,
|
||||
associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData),
|
||||
openGroupId: openGroup.id,
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
@ -805,10 +808,11 @@ public final class OpenGroupManager {
|
|||
if let messageInfo: MessageReceiveJob.Details.MessageInfo = processedMessage?.messageInfo {
|
||||
try MessageReceiver.handle(
|
||||
db,
|
||||
threadId: (lookup.sessionId ?? lookup.blindedId),
|
||||
threadVariant: .contact, // Technically not open group messages
|
||||
message: messageInfo.message,
|
||||
serverExpirationTimestamp: messageInfo.serverExpirationTimestamp,
|
||||
associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData),
|
||||
openGroupId: nil, // Intentionally nil as they are technically not open group messages
|
||||
dependencies: dependencies
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,7 +7,15 @@ import SessionUtilitiesKit
|
|||
import SessionSnodeKit
|
||||
|
||||
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 {
|
||||
case .preOffer: try MessageReceiver.handleNewCallMessage(db, message: message)
|
||||
case .offer: MessageReceiver.handleOfferCallMessage(db, message: message)
|
||||
|
@ -43,12 +51,18 @@ extension MessageReceiver {
|
|||
guard
|
||||
CurrentAppContext().isMainApp,
|
||||
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 }
|
||||
guard let timestamp = message.sentTimestamp, TimestampUtils.isWithinOneMinute(timestamp: timestamp) else {
|
||||
// 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) {
|
||||
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?
|
||||
.notifyUser(
|
||||
|
@ -62,7 +76,8 @@ extension MessageReceiver {
|
|||
|
||||
guard db[.areCallsEnabled] else {
|
||||
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?
|
||||
.notifyUser(
|
||||
|
|
|
@ -68,7 +68,6 @@ extension MessageReceiver {
|
|||
guard hasApprovedAdmin else { return }
|
||||
|
||||
// Create the group
|
||||
let groupAlreadyExisted: Bool = ((try? SessionThread.exists(db, id: groupPublicKey)) ?? false)
|
||||
let thread: SessionThread = try SessionThread
|
||||
.fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup)
|
||||
.with(shouldBeVisible: true)
|
||||
|
@ -84,39 +83,23 @@ extension MessageReceiver {
|
|||
try closedGroup.zombies.deleteAll(db)
|
||||
}
|
||||
|
||||
// Notify the user
|
||||
if !groupAlreadyExisted {
|
||||
// Create the GroupMember records
|
||||
try members.forEach { memberId in
|
||||
try GroupMember(
|
||||
groupId: groupPublicKey,
|
||||
profileId: memberId,
|
||||
role: .standard,
|
||||
isHidden: false
|
||||
).save(db)
|
||||
}
|
||||
|
||||
try admins.forEach { adminId in
|
||||
try GroupMember(
|
||||
groupId: groupPublicKey,
|
||||
profileId: adminId,
|
||||
role: .admin,
|
||||
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)
|
||||
// Create the GroupMember records if needed
|
||||
try members.forEach { memberId in
|
||||
try GroupMember(
|
||||
groupId: groupPublicKey,
|
||||
profileId: memberId,
|
||||
role: .standard,
|
||||
isHidden: false
|
||||
).save(db)
|
||||
}
|
||||
|
||||
try admins.forEach { adminId in
|
||||
try GroupMember(
|
||||
groupId: groupPublicKey,
|
||||
profileId: adminId,
|
||||
role: .admin,
|
||||
isHidden: false
|
||||
).save(db)
|
||||
}
|
||||
|
||||
// Update the DisappearingMessages config
|
||||
|
@ -194,12 +177,20 @@ extension MessageReceiver {
|
|||
}
|
||||
|
||||
do {
|
||||
try ClosedGroupKeyPair(
|
||||
let keyPair: ClosedGroupKeyPair = ClosedGroupKeyPair(
|
||||
threadId: groupPublicKey,
|
||||
publicKey: proto.publicKey.removingIdPrefixIfNeeded(),
|
||||
secretKey: proto.privateKey,
|
||||
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
).insert(db)
|
||||
)
|
||||
try keyPair.insert(db)
|
||||
|
||||
// Update libSession
|
||||
try? SessionUtil.update(
|
||||
db,
|
||||
groupPublicKey: groupPublicKey,
|
||||
latestKeyPair: keyPair
|
||||
)
|
||||
}
|
||||
catch {
|
||||
if case DatabaseError.SQLITE_CONSTRAINT_UNIQUE = error {
|
||||
|
@ -236,6 +227,13 @@ extension MessageReceiver {
|
|||
SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
).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 }
|
||||
|
||||
try performIfValid(db, message: message) { id, sender, thread, closedGroup in
|
||||
guard let groupMembers: [GroupMember] = try? closedGroup.members.fetchAll(db) else { return }
|
||||
guard let groupAdmins: [GroupMember] = try? closedGroup.admins.fetchAll(db) else { return }
|
||||
guard let allGroupMembers: [GroupMember] = try? closedGroup.allMembers.fetchAll(db) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Update the group
|
||||
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)
|
||||
|
||||
// Create records for any new members
|
||||
|
@ -278,7 +280,7 @@ extension MessageReceiver {
|
|||
// generated by the admin when they saw the member removed message.
|
||||
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
|
||||
MessageSender.sendLatestEncryptionKeyPair(db, to: memberId, for: id)
|
||||
}
|
||||
|
@ -292,7 +294,7 @@ extension MessageReceiver {
|
|||
.deleteAll(db)
|
||||
|
||||
// Notify the user if needed
|
||||
guard members != Set(groupMembers.map { $0.profileId }) else { return }
|
||||
guard members != currentMemberIds else { return }
|
||||
|
||||
_ = try Interaction(
|
||||
serverHash: message.serverHash,
|
||||
|
@ -303,7 +305,7 @@ extension MessageReceiver {
|
|||
.membersAdded(
|
||||
members: addedMembers
|
||||
.asSet()
|
||||
.subtracting(groupMembers.map { $0.profileId })
|
||||
.subtracting(currentMemberIds)
|
||||
.map { Data(hex: $0) }
|
||||
)
|
||||
.infoMessage(db, sender: sender),
|
||||
|
@ -312,6 +314,21 @@ extension MessageReceiver {
|
|||
SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
).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
|
||||
// Check that the admin wasn't removed
|
||||
guard let groupMembers: [GroupMember] = try? closedGroup.members.fetchAll(db) else { return }
|
||||
guard let groupAdmins: [GroupMember] = try? closedGroup.admins.fetchAll(db) else { return }
|
||||
guard let allGroupMembers: [GroupMember] = try? closedGroup.allMembers.fetchAll(db) else {
|
||||
return
|
||||
}
|
||||
|
||||
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.")
|
||||
}
|
||||
// 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.")
|
||||
}
|
||||
|
||||
|
@ -368,7 +390,7 @@ extension MessageReceiver {
|
|||
}
|
||||
|
||||
// Notify the user if needed
|
||||
guard members != Set(groupMembers.map { $0.profileId }) else { return }
|
||||
guard members != currentMemberIds else { return }
|
||||
|
||||
_ = try Interaction(
|
||||
serverHash: message.serverHash,
|
||||
|
@ -379,7 +401,7 @@ extension MessageReceiver {
|
|||
.membersRemoved(
|
||||
members: removedMembers
|
||||
.asSet()
|
||||
.intersection(groupMembers.map { $0.profileId })
|
||||
.intersection(currentMemberIds)
|
||||
.map { Data(hex: $0) }
|
||||
)
|
||||
.infoMessage(db, sender: sender),
|
||||
|
@ -388,6 +410,21 @@ extension MessageReceiver {
|
|||
SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
).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()
|
||||
)
|
||||
).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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,17 +5,21 @@ import GRDB
|
|||
import SessionSnodeKit
|
||||
|
||||
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
|
||||
threadVariant == .contact,
|
||||
let sender: String = message.sender,
|
||||
let messageKind: DataExtractionNotification.Kind = message.kind,
|
||||
let thread: SessionThread = try? SessionThread.fetchOne(db, id: sender),
|
||||
thread.variant == .contact
|
||||
let messageKind: DataExtractionNotification.Kind = message.kind
|
||||
else { return }
|
||||
|
||||
_ = try Interaction(
|
||||
serverHash: message.serverHash,
|
||||
threadId: thread.id,
|
||||
threadId: threadId,
|
||||
authorId: sender,
|
||||
variant: {
|
||||
switch messageKind {
|
||||
|
|
|
@ -5,12 +5,16 @@ import GRDB
|
|||
import SessionUtilitiesKit
|
||||
|
||||
extension MessageReceiver {
|
||||
internal static func handleExpirationTimerUpdate(_ db: Database, message: ExpirationTimerUpdate) throws {
|
||||
// Get the target thread
|
||||
internal static func handleExpirationTimerUpdate(
|
||||
_ db: Database,
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
message: ExpirationTimerUpdate
|
||||
) throws {
|
||||
guard
|
||||
let targetId: String = MessageReceiver.threadInfo(db, message: message, openGroupId: nil)?.id,
|
||||
let sender: String = message.sender,
|
||||
let thread: SessionThread = try? SessionThread.fetchOne(db, id: targetId)
|
||||
// Only process these for contact and legacy groups (new groups handle it separately)
|
||||
(threadVariant == .contact || threadVariant == .legacyGroup),
|
||||
let sender: String = message.sender
|
||||
else { return }
|
||||
|
||||
// Update the configuration
|
||||
|
@ -18,9 +22,10 @@ extension MessageReceiver {
|
|||
// 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
|
||||
// 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)
|
||||
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(thread.id))
|
||||
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(threadId))
|
||||
.with(
|
||||
// If there is no duration then we should disable the expiration timer
|
||||
isEnabled: ((message.duration ?? 0) > 0),
|
||||
|
@ -33,7 +38,7 @@ extension MessageReceiver {
|
|||
// Add an info message for the user
|
||||
_ = try Interaction(
|
||||
serverHash: nil, // Intentionally null so sync messages are seen as duplicates
|
||||
threadId: thread.id,
|
||||
threadId: threadId,
|
||||
authorId: sender,
|
||||
variant: .infoDisappearingMessagesUpdate,
|
||||
body: config.messageInfoString(
|
||||
|
|
|
@ -45,7 +45,8 @@ extension MessageReceiver {
|
|||
}
|
||||
|
||||
// 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
|
||||
// the blinded ids of any threads)
|
||||
|
|
|
@ -5,17 +5,19 @@ import GRDB
|
|||
import SessionUtilitiesKit
|
||||
|
||||
extension MessageReceiver {
|
||||
internal static func handleTypingIndicator(_ db: Database, message: TypingIndicator) throws {
|
||||
guard
|
||||
let senderPublicKey: String = message.sender,
|
||||
let thread: SessionThread = try SessionThread.fetchOne(db, id: senderPublicKey)
|
||||
else { return }
|
||||
internal static func handleTypingIndicator(
|
||||
_ db: Database,
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
message: TypingIndicator
|
||||
) throws {
|
||||
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else { return }
|
||||
|
||||
switch message.kind {
|
||||
case .started:
|
||||
let needsToStartTypingIndicator: Bool = TypingIndicators.didStartTypingNeedsToStart(
|
||||
threadId: thread.id,
|
||||
threadVariant: thread.variant,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
threadIsMessageRequest: thread.isMessageRequest(db),
|
||||
direction: .incoming,
|
||||
timestampMs: message.sentTimestamp.map { Int64($0) }
|
||||
|
|
|
@ -6,7 +6,12 @@ import SessionSnodeKit
|
|||
import SessionUtilitiesKit
|
||||
|
||||
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)
|
||||
|
||||
guard message.sender == message.author || userPublicKey == message.sender else { return }
|
||||
|
@ -19,12 +24,7 @@ extension MessageReceiver {
|
|||
|
||||
guard
|
||||
let interactionId: Int64 = maybeInteraction?.id,
|
||||
let interaction: Interaction = maybeInteraction,
|
||||
let threadVariant: SessionThread.Variant = try SessionThread
|
||||
.filter(id: interaction.threadId)
|
||||
.select(.variant)
|
||||
.asRequest(of: SessionThread.Variant.self)
|
||||
.fetchOne(db)
|
||||
let interaction: Interaction = maybeInteraction
|
||||
else { return }
|
||||
|
||||
// Mark incoming messages as read and remove any of their notifications
|
||||
|
|
|
@ -8,9 +8,10 @@ import SessionUtilitiesKit
|
|||
extension MessageReceiver {
|
||||
@discardableResult public static func handleVisibleMessage(
|
||||
_ db: Database,
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
message: VisibleMessage,
|
||||
associatedWithProto proto: SNProtoContent,
|
||||
openGroupId: String?,
|
||||
dependencies: Dependencies = Dependencies()
|
||||
) throws -> Int64 {
|
||||
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
|
||||
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
|
||||
let thread: SessionThread = try SessionThread
|
||||
.fetchOrCreate(db, id: threadInfo.id, variant: threadInfo.variant)
|
||||
let maybeOpenGroup: OpenGroup? = openGroupId.map { try? OpenGroup.fetchOne(db, id: $0) }
|
||||
.fetchOrCreate(db, id: threadId, variant: threadVariant, shouldBeVisible: nil)
|
||||
let maybeOpenGroup: OpenGroup? = {
|
||||
guard threadVariant == .community else { return nil }
|
||||
|
||||
return try? OpenGroup.fetchOne(db, id: threadId)
|
||||
}()
|
||||
let variant: Interaction.Variant = {
|
||||
guard
|
||||
let senderSessionId: SessionId = SessionId(from: sender),
|
||||
|
@ -94,7 +99,14 @@ extension MessageReceiver {
|
|||
}()
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -314,12 +326,11 @@ extension MessageReceiver {
|
|||
|
||||
private static func handleEmojiReactIfNeeded(
|
||||
_ db: Database,
|
||||
thread: SessionThread,
|
||||
message: VisibleMessage,
|
||||
associatedWithProto proto: SNProtoContent,
|
||||
sender: String,
|
||||
messageSentTimestamp: TimeInterval,
|
||||
openGroupId: String?,
|
||||
thread: SessionThread
|
||||
messageSentTimestamp: TimeInterval
|
||||
) throws -> Int64? {
|
||||
guard
|
||||
let reaction: VisibleMessage.VMReaction = message.reaction,
|
||||
|
@ -347,7 +358,7 @@ extension MessageReceiver {
|
|||
|
||||
switch reaction.kind {
|
||||
case .react:
|
||||
let reaction = Reaction(
|
||||
let reaction: Reaction = try Reaction(
|
||||
interactionId: interactionId,
|
||||
serverHash: message.serverHash,
|
||||
timestampMs: Int64(messageSentTimestamp * 1000),
|
||||
|
@ -355,8 +366,8 @@ extension MessageReceiver {
|
|||
emoji: reaction.emoji,
|
||||
count: 1,
|
||||
sortId: sortId
|
||||
)
|
||||
try reaction.insert(db)
|
||||
).inserted(db)
|
||||
|
||||
if sender != getUserHexEncodedPublicKey(db) {
|
||||
Environment.shared?.notificationsManager.wrappedValue?
|
||||
.notifyUser(
|
||||
|
|
|
@ -15,7 +15,7 @@ extension MessageSender {
|
|||
_ db: Database,
|
||||
name: String,
|
||||
members: Set<String>
|
||||
) -> AnyPublisher<SessionThread, Error> {
|
||||
) throws -> AnyPublisher<SessionThread, Error> {
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
var members: Set<String> = members
|
||||
|
||||
|
@ -30,89 +30,85 @@ extension MessageSender {
|
|||
|
||||
// Create the group
|
||||
members.insert(userPublicKey) // Ensure the current user is included in the member list
|
||||
let membersAsData = members.map { Data(hex: $0) }
|
||||
let admins = [ userPublicKey ]
|
||||
let adminsAsData = admins.map { Data(hex: $0) }
|
||||
let membersAsData: [Data] = members.map { Data(hex: $0) }
|
||||
let admins: Set<String> = [ userPublicKey ]
|
||||
let adminsAsData: [Data] = admins.map { Data(hex: $0) }
|
||||
let formationTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
let thread: SessionThread
|
||||
let memberSendData: [MessageSender.PreparedSendData]
|
||||
|
||||
do {
|
||||
// Create the relevant objects in the database
|
||||
thread = try SessionThread
|
||||
.fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup)
|
||||
try ClosedGroup(
|
||||
threadId: groupPublicKey,
|
||||
name: name,
|
||||
formationTimestamp: formationTimestamp
|
||||
// Create the relevant objects in the database
|
||||
let thread: SessionThread = try SessionThread
|
||||
.fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup, shouldBeVisible: true)
|
||||
try ClosedGroup(
|
||||
threadId: groupPublicKey,
|
||||
name: name,
|
||||
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)
|
||||
|
||||
// Store the key pair
|
||||
try ClosedGroupKeyPair(
|
||||
threadId: groupPublicKey,
|
||||
publicKey: encryptionKeyPair.publicKey,
|
||||
secretKey: encryptionKeyPair.privateKey,
|
||||
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
}
|
||||
|
||||
try members.forEach { memberId in
|
||||
try GroupMember(
|
||||
groupId: groupPublicKey,
|
||||
profileId: memberId,
|
||||
role: .standard,
|
||||
isHidden: false
|
||||
).insert(db)
|
||||
|
||||
// Create the member objects
|
||||
try admins.forEach { adminId in
|
||||
try GroupMember(
|
||||
groupId: groupPublicKey,
|
||||
profileId: adminId,
|
||||
role: .admin,
|
||||
isHidden: false
|
||||
).insert(db)
|
||||
}
|
||||
|
||||
try members.forEach { memberId in
|
||||
try GroupMember(
|
||||
groupId: groupPublicKey,
|
||||
profileId: memberId,
|
||||
role: .standard,
|
||||
isHidden: false
|
||||
).insert(db)
|
||||
}
|
||||
|
||||
// Notify the user
|
||||
//
|
||||
// Note: Intentionally don't want a 'serverHash' for closed group creation
|
||||
_ = try Interaction(
|
||||
threadId: thread.id,
|
||||
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
|
||||
}
|
||||
|
||||
// Update libSession
|
||||
try SessionUtil.add(
|
||||
db,
|
||||
groupPublicKey: groupPublicKey,
|
||||
name: name,
|
||||
latestKeyPairPublicKey: encryptionKeyPair.publicKey,
|
||||
latestKeyPairSecretKey: encryptionKeyPair.privateKey,
|
||||
latestKeyPairReceivedTimestamp: latestKeyPairReceivedTimestamp,
|
||||
members: members,
|
||||
admins: admins
|
||||
)
|
||||
|
||||
let memberSendData: [MessageSender.PreparedSendData] = 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
|
||||
),
|
||||
// Note: We set this here to ensure the value matches
|
||||
// the 'ClosedGroup' object we created
|
||||
sentTimestampMs: UInt64(floor(formationTimestamp * 1000))
|
||||
members: membersAsData,
|
||||
admins: adminsAsData,
|
||||
expirationTimer: 0
|
||||
),
|
||||
to: .contact(publicKey: memberId),
|
||||
interactionId: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return Fail(error: error)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
// Note: We set this here to ensure the value matches
|
||||
// the 'ClosedGroup' object we created
|
||||
sentTimestampMs: UInt64(floor(formationTimestamp * 1000))
|
||||
),
|
||||
to: .contact(publicKey: memberId),
|
||||
namespace: Message.Destination.contact(publicKey: memberId).defaultNamespace,
|
||||
interactionId: nil
|
||||
)
|
||||
}
|
||||
|
||||
return Publishers
|
||||
.MergeMany(
|
||||
|
@ -207,6 +203,7 @@ extension MessageSender {
|
|||
)
|
||||
),
|
||||
to: try Message.Destination.from(db, thread: thread),
|
||||
namespace: try Message.Destination.from(db, thread: thread).defaultNamespace,
|
||||
interactionId: nil
|
||||
)
|
||||
}
|
||||
|
@ -225,6 +222,21 @@ extension MessageSender {
|
|||
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 {
|
||||
if let index = ($0[closedGroup.id] ?? []).firstIndex(of: newKeyPair) {
|
||||
$0[closedGroup.id] = ($0[closedGroup.id] ?? [])
|
||||
|
@ -284,6 +296,13 @@ extension MessageSender {
|
|||
interactionId: interactionId,
|
||||
in: thread
|
||||
)
|
||||
|
||||
// Update libSession
|
||||
try? SessionUtil.update(
|
||||
db,
|
||||
groupPublicKey: closedGroup.threadId,
|
||||
name: name
|
||||
)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
|
@ -386,6 +405,20 @@ extension MessageSender {
|
|||
|
||||
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
|
||||
try MessageSender.send(
|
||||
db,
|
||||
|
@ -399,7 +432,7 @@ extension MessageSender {
|
|||
try addedMembers.forEach { member in
|
||||
// Send updates to the new members individually
|
||||
let thread: SessionThread = try SessionThread
|
||||
.fetchOrCreate(db, id: member, variant: .contact)
|
||||
.fetchOrCreate(db, id: member, variant: .contact, shouldBeVisible: nil)
|
||||
|
||||
try MessageSender.send(
|
||||
db,
|
||||
|
@ -505,6 +538,7 @@ extension MessageSender {
|
|||
)
|
||||
),
|
||||
to: try Message.Destination.from(db, thread: thread),
|
||||
namespace: try Message.Destination.from(db, thread: thread).defaultNamespace,
|
||||
interactionId: interactionId
|
||||
)
|
||||
)
|
||||
|
@ -571,6 +605,7 @@ extension MessageSender {
|
|||
kind: .memberLeft
|
||||
),
|
||||
to: try Message.Destination.from(db, thread: thread),
|
||||
namespace: try Message.Destination.from(db, thread: thread).defaultNamespace,
|
||||
interactionId: interactionId
|
||||
)
|
||||
|
||||
|
@ -594,6 +629,12 @@ extension MessageSender {
|
|||
}
|
||||
}
|
||||
catch {
|
||||
try? ClosedGroup.removeKeysAndUnsubscribe(
|
||||
db,
|
||||
threadId: groupPublicKey,
|
||||
removeGroupData: false,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
return Fail(error: error)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
@ -651,7 +692,7 @@ extension MessageSender {
|
|||
).build()
|
||||
let plaintext = try proto.serializedData()
|
||||
let thread: SessionThread = try SessionThread
|
||||
.fetchOrCreate(db, id: publicKey, variant: .contact)
|
||||
.fetchOrCreate(db, id: publicKey, variant: .contact, shouldBeVisible: nil)
|
||||
let ciphertext = try MessageSender.encryptWithSessionProtocol(
|
||||
db,
|
||||
plaintext: plaintext,
|
||||
|
|
|
@ -19,7 +19,7 @@ public enum MessageReceiver {
|
|||
isOutgoing: Bool? = nil,
|
||||
otherBlindedPublicKey: String? = nil,
|
||||
dependencies: SMKDependencies = SMKDependencies()
|
||||
) throws -> (Message, SNProtoContent, String) {
|
||||
) throws -> (Message, SNProtoContent, String, SessionThread.Variant) {
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
|
||||
let isOpenGroupMessage: Bool = (openGroupId != nil)
|
||||
|
||||
|
@ -147,7 +147,6 @@ public enum MessageReceiver {
|
|||
message.recipient = userPublicKey
|
||||
message.sentTimestamp = envelope.timestamp
|
||||
message.receivedTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs())
|
||||
message.groupPublicKey = groupPublicKey
|
||||
message.openGroupServerMessageId = openGroupMessageServerId.map { UInt64($0) }
|
||||
|
||||
// Validate
|
||||
|
@ -161,28 +160,29 @@ public enum MessageReceiver {
|
|||
}
|
||||
|
||||
// Extract the proper threadId for the message
|
||||
let threadId: String = {
|
||||
if let groupPublicKey: String = groupPublicKey { return groupPublicKey }
|
||||
if let openGroupId: String = openGroupId { return openGroupId }
|
||||
let (threadId, threadVariant): (String, SessionThread.Variant) = {
|
||||
if let groupPublicKey: String = groupPublicKey { return (groupPublicKey, .legacyGroup) }
|
||||
if let openGroupId: String = openGroupId { return (openGroupId, .community) }
|
||||
|
||||
switch message {
|
||||
case let message as VisibleMessage: return (message.syncTarget ?? sender)
|
||||
case let message as ExpirationTimerUpdate: return (message.syncTarget ?? sender)
|
||||
default: return sender
|
||||
case let message as VisibleMessage: return ((message.syncTarget ?? sender), .contact)
|
||||
case let message as ExpirationTimerUpdate: return ((message.syncTarget ?? sender), .contact)
|
||||
default: return (sender, .contact)
|
||||
}
|
||||
}()
|
||||
|
||||
return (message, proto, threadId)
|
||||
return (message, proto, threadId, threadVariant)
|
||||
}
|
||||
|
||||
// MARK: - Handling
|
||||
|
||||
public static func handle(
|
||||
_ db: Database,
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
message: Message,
|
||||
serverExpirationTimestamp: TimeInterval?,
|
||||
associatedWithProto proto: SNProtoContent,
|
||||
openGroupId: String?,
|
||||
dependencies: SMKDependencies = SMKDependencies()
|
||||
) throws {
|
||||
switch message {
|
||||
|
@ -194,35 +194,70 @@ public enum MessageReceiver {
|
|||
)
|
||||
|
||||
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:
|
||||
try MessageReceiver.handleClosedGroupControlMessage(db, message)
|
||||
try MessageReceiver.handleClosedGroupControlMessage(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
message: message
|
||||
)
|
||||
|
||||
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:
|
||||
try MessageReceiver.handleExpirationTimerUpdate(db, message: message)
|
||||
try MessageReceiver.handleExpirationTimerUpdate(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
message: message
|
||||
)
|
||||
|
||||
case let message as ConfigurationMessage:
|
||||
try MessageReceiver.handleConfigurationMessage(db, message: message)
|
||||
|
||||
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:
|
||||
try MessageReceiver.handleCallMessage(db, message: message)
|
||||
try MessageReceiver.handleCallMessage(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
message: message
|
||||
)
|
||||
|
||||
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:
|
||||
try MessageReceiver.handleVisibleMessage(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
message: message,
|
||||
associatedWithProto: proto,
|
||||
openGroupId: openGroupId
|
||||
associatedWithProto: proto
|
||||
)
|
||||
|
||||
// SharedConfigMessages should be handled by the 'SharedUtil' instead of this
|
||||
|
@ -232,13 +267,13 @@ public enum MessageReceiver {
|
|||
}
|
||||
|
||||
// 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(
|
||||
_ db: Database,
|
||||
message: Message,
|
||||
openGroupId: String?
|
||||
threadId: String,
|
||||
message: Message
|
||||
) throws {
|
||||
// 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)
|
||||
|
@ -246,14 +281,12 @@ public enum MessageReceiver {
|
|||
case is TypingIndicator: break
|
||||
|
||||
default:
|
||||
guard let threadInfo: (id: String, variant: SessionThread.Variant) = threadInfo(db, message: message, openGroupId: openGroupId) else {
|
||||
return
|
||||
}
|
||||
|
||||
_ = try SessionThread
|
||||
.fetchOrCreate(db, id: threadInfo.id, variant: threadInfo.variant)
|
||||
.with(shouldBeVisible: true)
|
||||
.saved(db)
|
||||
try SessionThread
|
||||
.filter(id: threadId)
|
||||
.updateAllAndConfig(
|
||||
db,
|
||||
SessionThread.Columns.shouldBeVisible.set(to: true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -281,36 +314,4 @@ public enum MessageReceiver {
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -284,7 +284,7 @@ public class Poller {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
.grouped { threadId, _, _ in (threadId ?? Message.nonThreadMessageId) }
|
||||
.grouped { threadId, _, _, _ in threadId }
|
||||
.forEach { threadId, threadMessages in
|
||||
messageCount += threadMessages.count
|
||||
processedMessages += threadMessages.map { $0.messageInfo.message }
|
||||
|
|
|
@ -787,7 +787,7 @@ public extension SessionThreadViewModel {
|
|||
let thread: TypedTableAlias<SessionThread> = 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 = {
|
||||
|
|
|
@ -206,12 +206,13 @@ class ConfigUserGroupsSpec {
|
|||
.to(equal("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"))
|
||||
|
||||
// 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)
|
||||
expect(legacyGroup3?.pointee).toNot(beNil())
|
||||
expect(config_needs_push(conf)).to(beTrue())
|
||||
expect(config_needs_dump(conf)).to(beTrue())
|
||||
ugroups_legacy_group_free(legacyGroup3)
|
||||
|
||||
let communityPubkey: String = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
var cCommunityPubkey: [UInt8] = Data(hex: communityPubkey).cArray
|
||||
|
@ -307,6 +308,7 @@ class ConfigUserGroupsSpec {
|
|||
}
|
||||
|
||||
ugroups_legacy_members_free(membersIt3)
|
||||
ugroups_legacy_group_free(legacyGroup4)
|
||||
|
||||
expect(membersSeen3).to(equal([
|
||||
"050000000000000000000000000000000000000000000000000000000000000000": false,
|
||||
|
@ -436,7 +438,7 @@ class ConfigUserGroupsSpec {
|
|||
expect(config_needs_dump(conf2)).to(beFalse())
|
||||
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_dump(conf2)).to(beTrue())
|
||||
|
||||
|
|
|
@ -75,23 +75,14 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
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 {
|
||||
case let visibleMessage as VisibleMessage:
|
||||
let interactionId: Int64 = try MessageReceiver.handleVisibleMessage(
|
||||
db,
|
||||
threadId: processedMessage.threadId,
|
||||
threadVariant: processedMessage.threadVariant,
|
||||
message: visibleMessage,
|
||||
associatedWithProto: processedMessage.proto,
|
||||
openGroupId: (isOpenGroup ? processedMessage.threadId : nil)
|
||||
associatedWithProto: processedMessage.proto
|
||||
)
|
||||
|
||||
// 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:
|
||||
try MessageReceiver.handleUnsendRequest(db, message: unsendRequest)
|
||||
try MessageReceiver.handleUnsendRequest(
|
||||
db,
|
||||
threadId: processedMessage.threadId,
|
||||
threadVariant: processedMessage.threadVariant,
|
||||
message: unsendRequest
|
||||
)
|
||||
|
||||
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:
|
||||
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() }
|
||||
|
||||
if !db[.areCallsEnabled] {
|
||||
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?
|
||||
.notifyUser(
|
||||
|
@ -146,7 +158,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
try SessionUtil.handleConfigMessages(
|
||||
db,
|
||||
messages: [sharedConfigMessage],
|
||||
publicKey: (processedMessage.threadId ?? "")
|
||||
publicKey: processedMessage.threadId
|
||||
)
|
||||
|
||||
default: break
|
||||
|
@ -155,8 +167,8 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
// Perform any required post-handling logic
|
||||
try MessageReceiver.postHandleMessage(
|
||||
db,
|
||||
message: processedMessage.messageInfo.message,
|
||||
openGroupId: (isOpenGroup ? processedMessage.threadId : nil)
|
||||
threadId: processedMessage.threadId,
|
||||
message: processedMessage.messageInfo.message
|
||||
)
|
||||
}
|
||||
catch {
|
||||
|
|
Loading…
Reference in New Issue