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

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

View File

@ -457,7 +457,7 @@ extension ConversationVC:
// Update the thread to be visible
_ = 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 {

View File

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

View File

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

View File

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

View File

@ -770,20 +770,13 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
// Delay the change to give the cell "unswipe" animation some time to complete
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))
)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -146,43 +146,50 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord,
}
}
// MARK: - Mutation
public extension SessionThread {
func with(
shouldBeVisible: Bool? = nil,
pinnedPriority: Int32? = nil
) -> SessionThread {
return SessionThread(
id: id,
variant: variant,
creationDateTimestamp: creationDateTimestamp,
shouldBeVisible: (shouldBeVisible ?? self.shouldBeVisible),
messageDraft: messageDraft,
notificationSound: notificationSound,
mutedUntilTimestamp: mutedUntilTimestamp,
onlyNotifyForMentions: onlyNotifyForMentions,
markedAsUnread: markedAsUnread,
pinnedPriority: (pinnedPriority ?? self.pinnedPriority)
)
}
}
// MARK: - GRDB Interactions
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

View File

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

View File

@ -157,9 +157,6 @@ public enum MessageSendJob: JobExecutor {
// Store the sentTimestamp from the message in case it fails due to a clockOutOfSync error
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -118,8 +118,8 @@ bool config_needs_dump(const config_object* conf);
/// Struct containing a list of C strings. Typically where this is returned by this API it must be
/// 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

View File

@ -31,7 +31,11 @@ bool community_parse_full_url(
// may be NULL in which case it is not set (typically both pubkey arguments would be null for cases
// 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

View File

@ -56,6 +56,12 @@ int user_profile_get_nts_priority(const config_object* conf);
// Sets the current note-to-self priority level. Should be >= 0 (negatives will be set to 0).
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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