From e007870c3429716134d30d2315ce80f5da8a2c69 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 12 Aug 2022 08:32:31 +1000 Subject: [PATCH 1/6] Fixed a bug where disappearing messages weren't working for local outgoing messages --- .../ConversationVC+Interaction.swift | 16 ++++++++++++++-- Session/Notifications/AppNotifications.swift | 8 +++++++- .../Database/Models/OpenGroup.swift | 6 ++++++ SessionShareExtension/ThreadPickerVC.swift | 6 ++++++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index caef86415..fab17974c 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -369,9 +369,15 @@ extension ConversationVC: body: text, timestampMs: sentTimestampMs, hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: text), + expiresInSeconds: try? DisappearingMessagesConfiguration + .select(.durationSeconds) + .filter(id: threadId) + .filter(DisappearingMessagesConfiguration.Columns.isEnabled == true) + .asRequest(of: TimeInterval.self) + .fetchOne(db), linkPreviewUrl: linkPreviewDraft?.urlString ).inserted(db) - + // If there is a LinkPreview and it doesn't match an existing one then add it now if let linkPreviewDraft: LinkPreviewDraft = linkPreviewDraft, @@ -459,7 +465,13 @@ extension ConversationVC: variant: .standardOutgoing, body: text, timestampMs: sentTimestampMs, - hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: text) + hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: text), + expiresInSeconds: try? DisappearingMessagesConfiguration + .select(.durationSeconds) + .filter(id: threadId) + .filter(DisappearingMessagesConfiguration.Columns.isEnabled == true) + .asRequest(of: TimeInterval.self) + .fetchOne(db) ).inserted(db) try MessageSender.send( diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 96463a3f8..e894f5ad6 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -436,7 +436,13 @@ class NotificationActionHandler { variant: .standardOutgoing, body: replyText, timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)), - hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: replyText) + hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: replyText), + expiresInSeconds: try? DisappearingMessagesConfiguration + .select(.durationSeconds) + .filter(id: threadId) + .filter(DisappearingMessagesConfiguration.Columns.isEnabled == true) + .asRequest(of: TimeInterval.self) + .fetchOne(db) ).inserted(db) try Interaction.markAsRead( diff --git a/SessionMessagingKit/Database/Models/OpenGroup.swift b/SessionMessagingKit/Database/Models/OpenGroup.swift index 71912d32d..b9a879e07 100644 --- a/SessionMessagingKit/Database/Models/OpenGroup.swift +++ b/SessionMessagingKit/Database/Models/OpenGroup.swift @@ -233,6 +233,12 @@ public class SMKOpenGroup: NSObject { authorId: userId, variant: .standardOutgoing, timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)), + expiresInSeconds: try? DisappearingMessagesConfiguration + .select(.durationSeconds) + .filter(id: userId) + .filter(DisappearingMessagesConfiguration.Columns.isEnabled == true) + .asRequest(of: TimeInterval.self) + .fetchOne(db), linkPreviewUrl: urlString ) .saved(db) diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index b75214326..4d3352558 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -236,6 +236,12 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView body: body, timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)), hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: body), + expiresInSeconds: try? DisappearingMessagesConfiguration + .select(.durationSeconds) + .filter(id: threadId) + .filter(DisappearingMessagesConfiguration.Columns.isEnabled == true) + .asRequest(of: TimeInterval.self) + .fetchOne(db), linkPreviewUrl: (isSharingUrl ? attachments.first?.linkPreviewDraft?.urlString : nil) ).inserted(db) From d0acaa9c3a6a04aed4cb189cd34f8adeacb867ed Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Sat, 13 Aug 2022 10:30:15 +1000 Subject: [PATCH 2/6] Increased the version and build numbers --- Session.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index ed849e696..4b3ed256a 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -6818,7 +6818,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 366; + CURRENT_PROJECT_VERSION = 368; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6857,7 +6857,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.0.1; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -6890,7 +6890,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 366; + CURRENT_PROJECT_VERSION = 368; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6929,7 +6929,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.0.1; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; From 6d6d45b2833716e39c43c2a7d4054bf5baa5e14b Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 15 Aug 2022 10:35:52 +1000 Subject: [PATCH 3/6] Updated the ProfilePictureView to only use YYImage for Gif and WebP images Added support for animated OpenGroup images --- Session/Shared/FullConversationCell.swift | 6 +- .../SimplifiedConversationCell.swift | 2 +- SessionUtilitiesKit/Media/Data+Image.swift | 5 + SessionUtilitiesKit/Media/ImageFormat.swift | 1 + SessionUtilitiesKit/Media/NSData+Image.m | 2 +- .../Profile Pictures/ProfilePictureView.swift | 193 +++++++++++++----- 6 files changed, 156 insertions(+), 53 deletions(-) diff --git a/Session/Shared/FullConversationCell.swift b/Session/Shared/FullConversationCell.swift index ebf55e4c2..14faf6618 100644 --- a/Session/Shared/FullConversationCell.swift +++ b/Session/Shared/FullConversationCell.swift @@ -240,7 +240,7 @@ public final class FullConversationCell: UITableViewCell { profile: cellViewModel.profile, additionalProfile: cellViewModel.additionalProfile, threadVariant: cellViewModel.threadVariant, - openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) }, + openGroupProfilePictureData: cellViewModel.openGroupProfilePictureData, useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil) ) @@ -280,7 +280,7 @@ public final class FullConversationCell: UITableViewCell { profile: cellViewModel.profile, additionalProfile: cellViewModel.additionalProfile, threadVariant: cellViewModel.threadVariant, - openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) }, + openGroupProfilePictureData: cellViewModel.openGroupProfilePictureData, useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil) ) @@ -341,7 +341,7 @@ public final class FullConversationCell: UITableViewCell { profile: cellViewModel.profile, additionalProfile: cellViewModel.additionalProfile, threadVariant: cellViewModel.threadVariant, - openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) }, + openGroupProfilePictureData: cellViewModel.openGroupProfilePictureData, useFallbackPicture: ( cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil diff --git a/SessionShareExtension/SimplifiedConversationCell.swift b/SessionShareExtension/SimplifiedConversationCell.swift index 9bea5687a..852379361 100644 --- a/SessionShareExtension/SimplifiedConversationCell.swift +++ b/SessionShareExtension/SimplifiedConversationCell.swift @@ -94,7 +94,7 @@ final class SimplifiedConversationCell: UITableViewCell { profile: cellViewModel.profile, additionalProfile: cellViewModel.additionalProfile, threadVariant: cellViewModel.threadVariant, - openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) }, + openGroupProfilePictureData: cellViewModel.openGroupProfilePictureData, useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil), showMultiAvatarForClosedGroup: true ) diff --git a/SessionUtilitiesKit/Media/Data+Image.swift b/SessionUtilitiesKit/Media/Data+Image.swift index 57adb8a4d..429ed9d5f 100644 --- a/SessionUtilitiesKit/Media/Data+Image.swift +++ b/SessionUtilitiesKit/Media/Data+Image.swift @@ -34,6 +34,8 @@ public extension Data { case (0x42, 0x4d): return .bmp case (0x4D, 0x4D): return .tiff // Motorola byte order TIFF case (0x49, 0x49): return .tiff // Intel byte order TIFF + case (0x52, 0x49): return .webp // First two letters of WebP + default: return .unknown } } @@ -113,6 +115,9 @@ public extension Data { mimeType == OWSMimeTypeImageBmp1 || mimeType == OWSMimeTypeImageBmp2 ) + + case .webp: + return (mimeType == nil || mimeType == OWSMimeTypeImageWebp) } } diff --git a/SessionUtilitiesKit/Media/ImageFormat.swift b/SessionUtilitiesKit/Media/ImageFormat.swift index e31f408c8..1db97f1df 100644 --- a/SessionUtilitiesKit/Media/ImageFormat.swift +++ b/SessionUtilitiesKit/Media/ImageFormat.swift @@ -9,4 +9,5 @@ public enum ImageFormat { case tiff case jpeg case bmp + case webp } diff --git a/SessionUtilitiesKit/Media/NSData+Image.m b/SessionUtilitiesKit/Media/NSData+Image.m index 5af4610cd..fb13b9d20 100644 --- a/SessionUtilitiesKit/Media/NSData+Image.m +++ b/SessionUtilitiesKit/Media/NSData+Image.m @@ -328,7 +328,7 @@ typedef struct { // Intel byte order TIFF return ImageFormat_Tiff; } else if (byte0 == 0x52 && byte1 == 0x49) { - // First two letters of RIFF tag. + // First two letters of WebP tag. return ImageFormat_Webp; } diff --git a/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift b/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift index 5dd308a11..bb8b477b1 100644 --- a/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift +++ b/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift @@ -19,8 +19,61 @@ public final class ProfilePictureView: UIView { // MARK: - Components - private lazy var imageView = getImageView() - private lazy var additionalImageView = getImageView() + private lazy var imageContainerView: UIView = { + let result: UIView = UIView() + result.translatesAutoresizingMaskIntoConstraints = false + result.clipsToBounds = true + result.backgroundColor = Colors.unimportant + + return result + }() + + private lazy var imageView: UIImageView = { + let result: UIImageView = UIImageView() + result.translatesAutoresizingMaskIntoConstraints = false + result.contentMode = .scaleAspectFill + result.isHidden = true + + return result + }() + + private lazy var animatedImageView: YYAnimatedImageView = { + let result: YYAnimatedImageView = YYAnimatedImageView() + result.translatesAutoresizingMaskIntoConstraints = false + result.contentMode = .scaleAspectFill + result.isHidden = true + + return result + }() + + private lazy var additionalImageContainerView: UIView = { + let result: UIView = UIView() + result.translatesAutoresizingMaskIntoConstraints = false + result.clipsToBounds = true + result.backgroundColor = Colors.unimportant + result.layer.cornerRadius = (Values.smallProfilePictureSize / 2) + result.isHidden = true + + return result + }() + + private lazy var additionalImageView: UIImageView = { + let result: UIImageView = UIImageView() + result.translatesAutoresizingMaskIntoConstraints = false + result.contentMode = .scaleAspectFill + result.isHidden = true + + return result + }() + + private lazy var additionalAnimatedImageView: YYAnimatedImageView = { + let result: YYAnimatedImageView = YYAnimatedImageView() + result.translatesAutoresizingMaskIntoConstraints = false + result.contentMode = .scaleAspectFill + result.isHidden = true + + return result + }() // MARK: - Lifecycle @@ -35,27 +88,33 @@ public final class ProfilePictureView: UIView { } private func setUpViewHierarchy() { - // Set up image view - addSubview(imageView) - imageView.pin(.leading, to: .leading, of: self) - imageView.pin(.top, to: .top, of: self) - let imageViewSize = CGFloat(Values.mediumProfilePictureSize) - imageViewWidthConstraint = imageView.set(.width, to: imageViewSize) - imageViewHeightConstraint = imageView.set(.height, to: imageViewSize) - - // Set up additional image view - addSubview(additionalImageView) - additionalImageView.pin(.trailing, to: .trailing, of: self) - additionalImageView.pin(.bottom, to: .bottom, of: self) - let additionalImageViewSize = CGFloat(Values.smallProfilePictureSize) - additionalImageViewWidthConstraint = additionalImageView.set(.width, to: additionalImageViewSize) - additionalImageViewHeightConstraint = additionalImageView.set(.height, to: additionalImageViewSize) - additionalImageView.layer.cornerRadius = additionalImageViewSize / 2 + + addSubview(imageContainerView) + addSubview(additionalImageContainerView) + + imageContainerView.pin(.leading, to: .leading, of: self) + imageContainerView.pin(.top, to: .top, of: self) + imageViewWidthConstraint = imageContainerView.set(.width, to: imageViewSize) + imageViewHeightConstraint = imageContainerView.set(.height, to: imageViewSize) + additionalImageContainerView.pin(.trailing, to: .trailing, of: self) + additionalImageContainerView.pin(.bottom, to: .bottom, of: self) + additionalImageViewWidthConstraint = additionalImageContainerView.set(.width, to: additionalImageViewSize) + additionalImageViewHeightConstraint = additionalImageContainerView.set(.height, to: additionalImageViewSize) + + imageContainerView.addSubview(imageView) + imageContainerView.addSubview(animatedImageView) + additionalImageContainerView.addSubview(additionalImageView) + additionalImageContainerView.addSubview(additionalAnimatedImageView) + + imageView.pin(to: imageContainerView) + animatedImageView.pin(to: imageContainerView) + additionalImageView.pin(to: additionalImageContainerView) + additionalAnimatedImageView.pin(to: additionalImageContainerView) } - // FIXME: Remove this once we refactor the ConversationVC to Swift (use the HomeViewModel approach) + // FIXME: Remove this once we refactor the OWSConversationSettingsViewController to Swift (use the HomeViewModel approach) @objc(updateForThreadId:) public func update(forThreadId threadId: String?) { guard @@ -74,7 +133,7 @@ public final class ProfilePictureView: UIView { profile: viewModel.profile, additionalProfile: viewModel.additionalProfile, threadVariant: viewModel.threadVariant, - openGroupProfilePicture: viewModel.openGroupProfilePictureData.map { UIImage(data: $0) }, + openGroupProfilePictureData: viewModel.openGroupProfilePictureData, useFallbackPicture: ( viewModel.threadVariant == .openGroup && viewModel.openGroupProfilePictureData == nil @@ -88,7 +147,7 @@ public final class ProfilePictureView: UIView { profile: Profile? = nil, additionalProfile: Profile? = nil, threadVariant: SessionThread.Variant, - openGroupProfilePicture: UIImage? = nil, + openGroupProfilePictureData: Data? = nil, useFallbackPicture: Bool = false, showMultiAvatarForClosedGroup: Bool = false ) { @@ -101,20 +160,38 @@ public final class ProfilePictureView: UIView { } imageView.contentMode = .center - imageView.backgroundColor = UIColor(rgbHex: 0x353535) - imageView.layer.cornerRadius = (self.size / 2) + imageView.isHidden = false + animatedImageView.isHidden = true + imageContainerView.backgroundColor = UIColor(rgbHex: 0x353535) + imageContainerView.layer.cornerRadius = (self.size / 2) imageViewWidthConstraint.constant = self.size imageViewHeightConstraint.constant = self.size - additionalImageView.isHidden = true + additionalImageContainerView.isHidden = true + animatedImageView.image = nil additionalImageView.image = nil - additionalImageView.layer.cornerRadius = (self.size / 2) + additionalAnimatedImageView.image = nil + additionalImageView.isHidden = true + additionalAnimatedImageView.isHidden = true return } - guard !publicKey.isEmpty || openGroupProfilePicture != nil else { return } + guard !publicKey.isEmpty || openGroupProfilePictureData != nil else { return } - func getProfilePicture(of size: CGFloat, for publicKey: String, profile: Profile?) -> (image: UIImage, isTappable: Bool) { - if let profile: Profile = profile, let profileData: Data = ProfileManager.profileAvatar(profile: profile), let image: YYImage = YYImage(data: profileData) { - return (image, true) + func getProfilePicture(of size: CGFloat, for publicKey: String, profile: Profile?) -> (image: UIImage?, animatedImage: YYImage?, isTappable: Bool) { + if let profile: Profile = profile, let profileData: Data = ProfileManager.profileAvatar(profile: profile) { + let format: ImageFormat = profileData.guessedImageFormat + + let image: UIImage? = (format == .gif || format == .webp ? + nil : + UIImage(data: profileData) + ) + let animatedImage: YYImage? = (format != .gif && format != .webp ? + nil : + YYImage(data: profileData) + ) + + if image != nil || animatedImage != nil { + return (image, animatedImage, true) + } } return ( @@ -124,6 +201,7 @@ public final class ProfilePictureView: UIView { .defaulting(to: publicKey), size: size ), + nil, false ) } @@ -147,56 +225,75 @@ public final class ProfilePictureView: UIView { imageViewHeightConstraint.constant = targetSize additionalImageViewWidthConstraint.constant = targetSize additionalImageViewHeightConstraint.constant = targetSize - additionalImageView.isHidden = false + additionalImageContainerView.isHidden = false if let additionalProfile: Profile = additionalProfile { - additionalImageView.image = getProfilePicture( + let (image, animatedImage, _): (UIImage?, YYImage?, Bool) = getProfilePicture( of: targetSize, for: additionalProfile.id, profile: additionalProfile - ).image + ) + + // Set the images and show the appropriate imageView (non-animated should be + // visible if there is no image) + additionalImageView.image = image + additionalAnimatedImageView.image = animatedImage + additionalImageView.isHidden = (animatedImage != nil) + additionalAnimatedImageView.isHidden = (animatedImage == nil) } default: targetSize = self.size imageViewWidthConstraint.constant = targetSize imageViewHeightConstraint.constant = targetSize - additionalImageView.isHidden = true + additionalImageContainerView.isHidden = true additionalImageView.image = nil + additionalImageView.isHidden = true + additionalAnimatedImageView.image = nil + additionalAnimatedImageView.isHidden = true } // Set the image - if let openGroupProfilePicture: UIImage = openGroupProfilePicture { - imageView.image = openGroupProfilePicture + if let openGroupProfilePictureData: Data = openGroupProfilePictureData { + let format: ImageFormat = openGroupProfilePictureData.guessedImageFormat + + let image: UIImage? = (format == .gif || format == .webp ? + nil : + UIImage(data: openGroupProfilePictureData) + ) + let animatedImage: YYImage? = (format != .gif && format != .webp ? + nil : + YYImage(data: openGroupProfilePictureData) + ) + + imageView.image = image + animatedImageView.image = animatedImage + imageView.isHidden = (animatedImage != nil) + animatedImageView.isHidden = (animatedImage == nil) hasTappableProfilePicture = true } else { - let (image, isTappable): (UIImage, Bool) = getProfilePicture( + let (image, animatedImage, isTappable): (UIImage?, YYImage?, Bool) = getProfilePicture( of: targetSize, for: publicKey, profile: profile ) imageView.image = image + animatedImageView.image = animatedImage + imageView.isHidden = (animatedImage != nil) + animatedImageView.isHidden = (animatedImage == nil) hasTappableProfilePicture = isTappable } imageView.contentMode = .scaleAspectFill - imageView.backgroundColor = Colors.unimportant - imageView.layer.cornerRadius = (targetSize / 2) - additionalImageView.layer.cornerRadius = (targetSize / 2) + animatedImageView.contentMode = .scaleAspectFill + imageContainerView.backgroundColor = Colors.unimportant + imageContainerView.layer.cornerRadius = (targetSize / 2) + additionalImageContainerView.layer.cornerRadius = (targetSize / 2) } // MARK: - Convenience - private func getImageView() -> YYAnimatedImageView { - let result = YYAnimatedImageView() - result.layer.masksToBounds = true - result.backgroundColor = Colors.unimportant - result.contentMode = .scaleAspectFill - - return result - } - @objc public func getProfilePicture() -> UIImage? { return (hasTappableProfilePicture ? imageView.image : nil) } From 0ce96976bf3350bf83be828d16cc4062e52c9b46 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 15 Aug 2022 16:04:07 +1000 Subject: [PATCH 4/6] Fixed an issue where the open group seqNo wasn't updated for deletions --- Session.xcodeproj/project.pbxproj | 8 ++++---- SessionMessagingKit/Open Groups/OpenGroupManager.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 4b3ed256a..1cdcc7965 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -6818,7 +6818,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 368; + CURRENT_PROJECT_VERSION = 369; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6857,7 +6857,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.0.1; + MARKETING_VERSION = 2.0.2; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -6890,7 +6890,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 368; + CURRENT_PROJECT_VERSION = 369; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6929,7 +6929,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.0.1; + MARKETING_VERSION = 2.0.2; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 0c73af2cc..f1c1c6fcf 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -498,13 +498,13 @@ public final class OpenGroupManager: NSObject { return } + let seqNo: Int64? = messages.map { $0.seqNo }.max() let sortedMessages: [OpenGroupAPI.Message] = messages .filter { $0.deleted != true } .sorted { lhs, rhs in lhs.id < rhs.id } var messageServerIdsToRemove: [Int64] = messages .filter { $0.deleted == true } .map { $0.id } - let seqNo: Int64? = sortedMessages.map { $0.seqNo }.max() // Update the 'openGroupSequenceNumber' value (Note: SOGS V4 uses the 'seqNo' instead of the 'serverId') if let seqNo: Int64 = seqNo { From 9f4d1a678a8021696307e21d58abcddff2033873 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 16 Aug 2022 11:44:11 +1000 Subject: [PATCH 5/6] Fixed a bug where deleted incoming messages could incorrectly be counted as unread --- Session.xcodeproj/project.pbxproj | 4 +++ Session/Meta/AppDelegate.swift | 5 ++++ SessionMessagingKit/Configuration.swift | 3 +++ .../_005_FixDeletedMessageReadState.swift | 25 +++++++++++++++++++ .../DisappearingMessageConfiguration.swift | 2 +- .../Database/Models/Interaction.swift | 8 +++--- .../Database/Models/OpenGroup.swift | 2 +- .../MessageReceiver+Calls.swift | 1 + .../SessionThreadViewModel.swift | 4 +++ 9 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 1cdcc7965..c568bb312 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -644,6 +644,7 @@ FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5B12554AF9800555489 /* VisibleMessage+Profile.swift */; }; FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A31225574F5200338F3E /* MessageReceiveJob.swift */; }; FD245C6D285066A400B966DD /* NotifyPushServerJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A32E2557549C00338F3E /* NotifyPushServerJob.swift */; }; + FD37EA0D28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */; }; FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; }; FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */; }; FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */; }; @@ -1683,6 +1684,7 @@ FD245C612850664300B966DD /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; FD28A4F327EA79F800FF65E7 /* BlockListUIUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockListUIUtils.swift; sourceTree = ""; }; FD28A4F527EAD44C00FF65E7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; + FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_FixDeletedMessageReadState.swift; sourceTree = ""; }; FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = ""; }; FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequestInfoSpec.swift; sourceTree = ""; }; FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponseSpec.swift; sourceTree = ""; }; @@ -3454,6 +3456,7 @@ FDF0B7412804EA4F004C14C5 /* _002_SetupStandardJobs.swift */, FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */, FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */, + FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */, ); path = Migrations; sourceTree = ""; @@ -5191,6 +5194,7 @@ FD716E682850318E00C96BF4 /* CallMode.swift in Sources */, FD09799527FE7B8E00936362 /* Interaction.swift in Sources */, FD5C72FF284F0F120029977D /* MessageReceiver+ConfigurationMessages.swift in Sources */, + FD37EA0D28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift in Sources */, FDC4380927B31D4E00C60D73 /* OpenGroupAPIError.swift in Sources */, FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */, FDC438C927BB706500C60D73 /* SendDirectMessageRequest.swift in Sources */, diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index fca92e62e..3bbff0213 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -435,6 +435,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return try Interaction .filter(Interaction.Columns.wasRead == false) + .filter( + // Exclude outgoing and deleted messages from the count + Interaction.Columns.variant != Interaction.Variant.standardOutgoing && + Interaction.Columns.variant != Interaction.Variant.standardIncomingDeleted + ) .filter( // Only count mentions if 'onlyNotifyForMentions' is set thread[.onlyNotifyForMentions] == false || diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 22a726c2e..928133641 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -15,6 +15,9 @@ public enum SNMessagingKit { // Just to make the external API nice ], [ _004_RemoveLegacyYDB.self + ], + [ + _005_FixDeletedMessageReadState.self ] ] ) diff --git a/SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift b/SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift new file mode 100644 index 000000000..65e68507c --- /dev/null +++ b/SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift @@ -0,0 +1,25 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +/// This migration fixes a bug where certain message variants could incorrectly be counted as unread messages +enum _005_FixDeletedMessageReadState: Migration { + static let target: TargetMigrations.Identifier = .messagingKit + static let identifier: String = "FixDeletedMessageReadState" + static let needsConfigSync: Bool = false + static let minExpectedRunDuration: TimeInterval = 0.1 + + static func migrate(_ db: Database) throws { + _ = try Interaction + .filter( + Interaction.Columns.variant == Interaction.Variant.standardIncomingDeleted || + Interaction.Columns.variant == Interaction.Variant.standardOutgoing || + Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate + ) + .updateAll(db, Interaction.Columns.wasRead.set(to: true)) + + Storage.update(progress: 1, for: self, in: target) // In case this is the last migration + } +} diff --git a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift index 6d4b1cfbc..689162b2f 100644 --- a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift +++ b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift @@ -208,7 +208,7 @@ public class SMKDisappearingMessagesConfiguration: NSObject { body: config.messageInfoString(with: nil), timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) ) - .saved(db) + .inserted(db) try MessageSender.send( db, diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index 238816699..d94708b0d 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -262,7 +262,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu self.body = body self.timestampMs = timestampMs self.receivedAtTimestampMs = receivedAtTimestampMs - self.wasRead = wasRead + self.wasRead = (wasRead && variant.canBeUnread) self.hasMention = hasMention self.expiresInSeconds = expiresInSeconds self.expiresStartedAtMs = expiresStartedAtMs @@ -304,7 +304,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu default: return timestampMs } }() - self.wasRead = wasRead + self.wasRead = (wasRead && variant.canBeUnread) self.hasMention = hasMention self.expiresInSeconds = expiresInSeconds self.expiresStartedAtMs = expiresStartedAtMs @@ -497,8 +497,6 @@ public extension Interaction { .filter(Interaction.Columns.threadId == threadId) .filter(Interaction.Columns.timestampMs <= interactionInfo.timestampMs) .filter(Interaction.Columns.wasRead == false) - // The `wasRead` flag doesn't apply to `standardOutgoing` or `standardIncomingDeleted` - .filter(Columns.variant != Variant.standardOutgoing && Columns.variant != Variant.standardIncomingDeleted) let interactionIdsToMarkAsRead: [Int64] = try interactionQuery .select(.id) .asRequest(of: Int64.self) @@ -600,7 +598,7 @@ public extension Interaction { body: nil, timestampMs: timestampMs, receivedAtTimestampMs: receivedAtTimestampMs, - wasRead: wasRead, + wasRead: (wasRead && Variant.standardIncomingDeleted.canBeUnread), hasMention: hasMention, expiresInSeconds: expiresInSeconds, expiresStartedAtMs: expiresStartedAtMs, diff --git a/SessionMessagingKit/Database/Models/OpenGroup.swift b/SessionMessagingKit/Database/Models/OpenGroup.swift index b9a879e07..d2aae5f69 100644 --- a/SessionMessagingKit/Database/Models/OpenGroup.swift +++ b/SessionMessagingKit/Database/Models/OpenGroup.swift @@ -241,7 +241,7 @@ public class SMKOpenGroup: NSObject { .fetchOne(db), linkPreviewUrl: urlString ) - .saved(db) + .inserted(db) try MessageSender.send( db, diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift index de74c53f7..8dc60c126 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift @@ -193,6 +193,7 @@ extension MessageReceiver { ) ) .inserted(db) + try MessageSender .sendNonDurably( db, diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index bb78b6940..58e07f7e5 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -346,6 +346,8 @@ public extension SessionThreadViewModel { // MARK: --SessionThreadViewModel public extension SessionThreadViewModel { + /// **Note:** This query **will not** include deleted incoming messages in it's unread count (they should never be marked as unread + /// but including this warning just in case there is a discrepancy) static func baseQuery( userPublicKey: String, filterSQL: SQL, @@ -610,6 +612,8 @@ public extension SessionThreadViewModel { // MARK: - ConversationVC public extension SessionThreadViewModel { + /// **Note:** This query **will** include deleted incoming messages in it's unread count (they should never be marked as unread + /// but including this warning just in case there is a discrepancy) static func conversationQuery(threadId: String, userPublicKey: String) -> AdaptedFetchRequest> { let thread: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() From 3ab8bdec7726fecb52aa12eda2859d9dea8ddc62 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 16 Aug 2022 13:56:40 +1000 Subject: [PATCH 6/6] Fixed an issue where hidden mods/admins wouldn't be properly recognised Added an isHidden flag to the GroupMember Reset the OpenGroup 'infoUpdates' value to force a re-fetch of the data and fix users affected by this bug Fixed the broken unit tests --- Podfile | 3 + Podfile.lock | 2 +- Session.xcodeproj/project.pbxproj | 28 ++ Session/Utilities/MockDataGenerator.swift | 6 +- SessionMessagingKit/Configuration.swift | 3 +- .../Migrations/_003_YDBToGRDBMigration.swift | 9 +- .../_006_FixHiddenModAdminSupport.swift | 30 ++ .../Database/Models/GroupMember.swift | 6 +- .../Open Groups/OpenGroupManager.swift | 28 +- .../MessageReceiver+ClosedGroups.swift | 12 +- .../MessageSender+ClosedGroups.swift | 9 +- .../Open Groups/Models/CapabilitiesSpec.swift | 16 +- .../Open Groups/Models/OpenGroupSpec.swift | 2 +- .../Open Groups/OpenGroupAPISpec.swift | 2 + .../Open Groups/OpenGroupManagerSpec.swift | 276 ++++++++++++++++-- .../Database/Models/Identity.swift | 16 +- .../Database/Types/TypedTableAlteration.swift | 26 ++ .../Utilities/Database+Utilities.swift | 11 + .../Database/Models/IdentitySpec.swift | 105 +++++++ 19 files changed, 539 insertions(+), 51 deletions(-) create mode 100644 SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift create mode 100644 SessionUtilitiesKit/Database/Types/TypedTableAlteration.swift create mode 100644 SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift diff --git a/Podfile b/Podfile index ae568f035..7e448b202 100644 --- a/Podfile +++ b/Podfile @@ -66,6 +66,9 @@ abstract_target 'GlobalDependencies' do pod 'Quick' pod 'Nimble' + + # Need to include this for the tests because otherwise it won't actually build + pod 'YYImage/libwebp', git: 'https://github.com/signalapp/YYImage' end end diff --git a/Podfile.lock b/Podfile.lock index 37f4ac9c5..1315eb501 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -242,6 +242,6 @@ SPEC CHECKSUMS: YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: f0857369c4831b2e5c1946345e76e493f3286805 +PODFILE CHECKSUM: 0e694576fbda3c10bbc762998183d97142b85896 COCOAPODS: 1.11.3 diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index c568bb312..e77691409 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -645,6 +645,9 @@ FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A31225574F5200338F3E /* MessageReceiveJob.swift */; }; FD245C6D285066A400B966DD /* NotifyPushServerJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A32E2557549C00338F3E /* NotifyPushServerJob.swift */; }; FD37EA0D28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */; }; + FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */; }; + FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */; }; + FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */; }; FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; }; FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */; }; FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */; }; @@ -1685,6 +1688,9 @@ FD28A4F327EA79F800FF65E7 /* BlockListUIUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockListUIUtils.swift; sourceTree = ""; }; FD28A4F527EAD44C00FF65E7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_FixDeletedMessageReadState.swift; sourceTree = ""; }; + FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_FixHiddenModAdminSupport.swift; sourceTree = ""; }; + FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableAlteration.swift; sourceTree = ""; }; + FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentitySpec.swift; sourceTree = ""; }; FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = ""; }; FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequestInfoSpec.swift; sourceTree = ""; }; FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponseSpec.swift; sourceTree = ""; }; @@ -3457,6 +3463,7 @@ FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */, FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */, FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */, + FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */, ); path = Migrations; sourceTree = ""; @@ -3523,6 +3530,7 @@ FD17D7B727F51ECA00122BE0 /* Migration.swift */, FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */, FD17D7C027F5200100122BE0 /* TypedTableDefinition.swift */, + FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */, FD7162DA281B6C440060647B /* TypedTableAlias.swift */, FD848B8A283DC509000E298B /* PagedDatabaseObserver.swift */, ); @@ -3570,6 +3578,22 @@ path = LegacyDatabase; sourceTree = ""; }; + FD37EA1228AB3F60003AE748 /* Database */ = { + isa = PBXGroup; + children = ( + FD37EA1328AB42C1003AE748 /* Models */, + ); + path = Database; + sourceTree = ""; + }; + FD37EA1328AB42C1003AE748 /* Models */ = { + isa = PBXGroup; + children = ( + FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */, + ); + path = Models; + sourceTree = ""; + }; FD3C905D27E410DB00CD579F /* Common Networking */ = { isa = PBXGroup; children = ( @@ -3659,6 +3683,7 @@ FD83B9B027CF200A005E1583 /* SessionUtilitiesKitTests */ = { isa = PBXGroup; children = ( + FD37EA1228AB3F60003AE748 /* Database */, FD83B9B927CF20A5005E1583 /* General */, ); path = SessionUtilitiesKitTests; @@ -5018,6 +5043,7 @@ FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */, C3D9E4D12567777D0040E4F3 /* OWSMediaUtils.swift in Sources */, C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */, + FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */, C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */, C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */, C3D9E35E25675F640040E4F3 /* OWSFileSystem.m in Sources */, @@ -5125,6 +5151,7 @@ 7BFD1A8C2747150E00FB91B9 /* TurnServerInfo.swift in Sources */, FDC4386527B4DE7600C60D73 /* RoomPollInfo.swift in Sources */, FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */, + FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */, C3D9E3BF25676AD70040E4F3 /* (null) in Sources */, B8BF43BA26CC95FB007828D1 /* WebRTC+Utilities.swift in Sources */, FDC438A627BB113A00C60D73 /* UserUnbanRequest.swift in Sources */, @@ -5444,6 +5471,7 @@ FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */, FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */, FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, + FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */, FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Session/Utilities/MockDataGenerator.swift b/Session/Utilities/MockDataGenerator.swift index 9b0b12090..457a50b89 100644 --- a/Session/Utilities/MockDataGenerator.swift +++ b/Session/Utilities/MockDataGenerator.swift @@ -256,7 +256,8 @@ enum MockDataGenerator { _ = try! GroupMember( groupId: randomGroupPublicKey, profileId: memberId, - role: .standard + role: .standard, + isHidden: false ) .saved(db) } @@ -264,7 +265,8 @@ enum MockDataGenerator { _ = try! GroupMember( groupId: randomGroupPublicKey, profileId: adminId, - role: .admin + role: .admin, + isHidden: false ) .saved(db) } diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 928133641..2230a04de 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -17,7 +17,8 @@ public enum SNMessagingKit { // Just to make the external API nice _004_RemoveLegacyYDB.self ], [ - _005_FixDeletedMessageReadState.self + _005_FixDeletedMessageReadState.self, + _006_FixHiddenModAdminSupport.self ] ] ) diff --git a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift index e1c8d8b4e..b748da16d 100644 --- a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -650,7 +650,8 @@ enum _003_YDBToGRDBMigration: Migration { try GroupMember( groupId: threadId, profileId: memberId, - role: .standard + role: .standard, + isHidden: false ).insert(db) if !validProfileIds.contains(memberId) { @@ -662,7 +663,8 @@ enum _003_YDBToGRDBMigration: Migration { try GroupMember( groupId: threadId, profileId: adminId, - role: .admin + role: .admin, + isHidden: false ).insert(db) if !validProfileIds.contains(adminId) { @@ -674,7 +676,8 @@ enum _003_YDBToGRDBMigration: Migration { try GroupMember( groupId: threadId, profileId: zombieId, - role: .zombie + role: .zombie, + isHidden: false ).insert(db) if !validProfileIds.contains(zombieId) { diff --git a/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift b/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift new file mode 100644 index 000000000..c1097eb94 --- /dev/null +++ b/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift @@ -0,0 +1,30 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +/// This migration fixes an issue where hidden mods/admins weren't getting recognised as mods/admins, it reset's the `info_updates` +/// for open groups so they will fully re-fetch their mod/admin lists +enum _006_FixHiddenModAdminSupport: Migration { + static let target: TargetMigrations.Identifier = .messagingKit + static let identifier: String = "FixHiddenModAdminSupport" + static let needsConfigSync: Bool = false + static let minExpectedRunDuration: TimeInterval = 0.1 + + static func migrate(_ db: Database) throws { + try db.alter(table: GroupMember.self) { t in + t.add(.isHidden, .boolean) + .notNull() + .defaults(to: false) + } + + // When modifying OpenGroup behaviours we should always look to reset the `infoUpdates` + // value for all OpenGroups to ensure they all have the correct state for newly + // added/changed fields + _ = try OpenGroup + .updateAll(db, OpenGroup.Columns.infoUpdates.set(to: 0)) + + Storage.update(progress: 1, for: self, in: target) // In case this is the last migration + } +} diff --git a/SessionMessagingKit/Database/Models/GroupMember.swift b/SessionMessagingKit/Database/Models/GroupMember.swift index a59bfc417..4cfe0abd4 100644 --- a/SessionMessagingKit/Database/Models/GroupMember.swift +++ b/SessionMessagingKit/Database/Models/GroupMember.swift @@ -17,6 +17,7 @@ public struct GroupMember: Codable, Equatable, FetchableRecord, PersistableRecor case groupId case profileId case role + case isHidden } public enum Role: Int, Codable, DatabaseValueConvertible { @@ -29,6 +30,7 @@ public struct GroupMember: Codable, Equatable, FetchableRecord, PersistableRecor public let groupId: String public let profileId: String public let role: Role + public let isHidden: Bool // MARK: - Relationships @@ -49,11 +51,13 @@ public struct GroupMember: Codable, Equatable, FetchableRecord, PersistableRecor public init( groupId: String, profileId: String, - role: Role + role: Role, + isHidden: Bool ) { self.groupId = groupId self.profileId = profileId self.role = role + self.isHidden = isHidden } } diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index f1c1c6fcf..3e8370ddb 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -422,17 +422,41 @@ public final class OpenGroupManager: NSObject { _ = try GroupMember( groupId: threadId, profileId: adminId, - role: .admin + role: .admin, + isHidden: false ).saved(db) } + try roomDetails.hiddenAdmins + .defaulting(to: []) + .forEach { adminId in + _ = try GroupMember( + groupId: threadId, + profileId: adminId, + role: .admin, + isHidden: true + ).saved(db) + } + try roomDetails.moderators.forEach { moderatorId in _ = try GroupMember( groupId: threadId, profileId: moderatorId, - role: .moderator + role: .moderator, + isHidden: false ).saved(db) } + + try roomDetails.hiddenModerators + .defaulting(to: []) + .forEach { moderatorId in + _ = try GroupMember( + groupId: threadId, + profileId: moderatorId, + role: .moderator, + isHidden: true + ).saved(db) + } } db.afterNextTransactionCommit { db in diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift index e2312383d..20d52e23f 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift @@ -89,7 +89,8 @@ extension MessageReceiver { try GroupMember( groupId: groupPublicKey, profileId: memberId, - role: .standard + role: .standard, + isHidden: false ).save(db) } @@ -97,7 +98,8 @@ extension MessageReceiver { try GroupMember( groupId: groupPublicKey, profileId: adminId, - role: .admin + role: .admin, + isHidden: false ).save(db) } @@ -254,7 +256,8 @@ extension MessageReceiver { try GroupMember( groupId: id, profileId: memberId, - role: .standard + role: .standard, + isHidden: false ).insert(db) } @@ -440,7 +443,8 @@ extension MessageReceiver { try GroupMember( groupId: id, profileId: sender, - role: .zombie + role: .zombie, + isHidden: false ).insert(db) } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift index ff813332e..bc8d52a0b 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift @@ -37,7 +37,8 @@ extension MessageSender { try GroupMember( groupId: groupPublicKey, profileId: adminId, - role: .admin + role: .admin, + isHidden: false ).insert(db) } @@ -48,7 +49,8 @@ extension MessageSender { try GroupMember( groupId: groupPublicKey, profileId: memberId, - role: .standard + role: .standard, + isHidden: false ).insert(db) } @@ -374,7 +376,8 @@ extension MessageSender { try GroupMember( groupId: closedGroup.id, profileId: member, - role: .standard + role: .standard, + isHidden: false ).insert(db) } } diff --git a/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift b/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift index 707f8852d..220c9bad2 100644 --- a/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift @@ -37,7 +37,7 @@ class CapabilitiesSpec: QuickSpec { describe("a Capability") { context("when initializing") { it("succeeeds with a valid case") { - let capability: OpenGroupAPI.Capabilities.Capability = OpenGroupAPI.Capabilities.Capability( + let capability: Capability.Variant = Capability.Variant( from: "sogs" ) @@ -45,7 +45,7 @@ class CapabilitiesSpec: QuickSpec { } it("wraps an unknown value in the unsupported case") { - let capability: OpenGroupAPI.Capabilities.Capability = OpenGroupAPI.Capabilities.Capability( + let capability: Capability.Variant = Capability.Variant( from: "test" ) @@ -55,12 +55,12 @@ class CapabilitiesSpec: QuickSpec { context("when accessing the rawValue") { it("provides known cases exactly") { - expect(OpenGroupAPI.Capabilities.Capability.sogs.rawValue).to(equal("sogs")) - expect(OpenGroupAPI.Capabilities.Capability.blind.rawValue).to(equal("blind")) + expect(Capability.Variant.sogs.rawValue).to(equal("sogs")) + expect(Capability.Variant.blind.rawValue).to(equal("blind")) } it("provides the wrapped value for unsupported cases") { - expect(OpenGroupAPI.Capabilities.Capability.unsupported("test").rawValue).to(equal("test")) + expect(Capability.Variant.unsupported("test").rawValue).to(equal("test")) } } @@ -68,14 +68,14 @@ class CapabilitiesSpec: QuickSpec { it("decodes known cases exactly") { expect( try? JSONDecoder().decode( - OpenGroupAPI.Capabilities.Capability.self, + Capability.Variant.self, from: "\"sogs\"".data(using: .utf8)! ) ) .to(equal(.sogs)) expect( try? JSONDecoder().decode( - OpenGroupAPI.Capabilities.Capability.self, + Capability.Variant.self, from: "\"blind\"".data(using: .utf8)! ) ) @@ -85,7 +85,7 @@ class CapabilitiesSpec: QuickSpec { it("decodes unknown cases into the unsupported case") { expect( try? JSONDecoder().decode( - OpenGroupAPI.Capabilities.Capability.self, + Capability.Variant.self, from: "\"test\"".data(using: .utf8)! ) ) diff --git a/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift b/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift index 966a270c4..2b8e7c858 100644 --- a/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift @@ -76,7 +76,7 @@ class OpenGroupSpec: QuickSpec { ) expect(openGroup.debugDescription) - .to(equal("OpenGroup(server: \"server\", roomToken: \"room\", id: \"server.room\", publicKey: \"1234\", isActive: true, name: \"name\", roomDescription: null, imageId: null, userCount: 0, infoUpdates: 0, sequenceNumber: 0, inboxLatestMessageId: 0, outboxLatestMessageId: 0)")) + .to(equal("OpenGroup(server: \"server\", roomToken: \"room\", id: \"server.room\", publicKey: \"1234\", isActive: true, name: \"name\", roomDescription: null, imageId: null, userCount: 0, infoUpdates: 0, sequenceNumber: 0, inboxLatestMessageId: 0, outboxLatestMessageId: 0, pollFailureCount: 0)")) } } } diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index a67c9c3ee..e4edc8803 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -1237,6 +1237,7 @@ class OpenGroupAPISpec: QuickSpec { sender: "testSender", posted: 321, edited: nil, + deleted: nil, seqNo: 10, whisper: false, whisperMods: false, @@ -1605,6 +1606,7 @@ class OpenGroupAPISpec: QuickSpec { sender: "testSender", posted: 321, edited: nil, + deleted: nil, seqNo: 10, whisper: false, whisperMods: false, diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index e14a1099f..9a598a15b 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -186,6 +186,7 @@ class OpenGroupManagerSpec: QuickSpec { sender: "05\(TestConstants.publicKey)", posted: 123, edited: nil, + deleted: nil, seqNo: 124, whisper: false, whisperMods: false, @@ -1277,7 +1278,12 @@ class OpenGroupManagerSpec: QuickSpec { defaultWrite: nil, upload: false, defaultUpload: nil, - details: TestCapabilitiesAndRoomApi.roomData.with(moderators: ["TestMod"], admins: []) + details: TestCapabilitiesAndRoomApi.roomData.with( + moderators: ["TestMod"], + hiddenModerators: [], + admins: [], + hiddenAdmins: [] + ) ) mockStorage.write { db in @@ -1308,7 +1314,67 @@ class OpenGroupManagerSpec: QuickSpec { server: "testServer" ), profileId: "TestMod", - role: .moderator + role: .moderator, + isHidden: false + ) + )) + } + + it("updates for hidden moderators") { + var didComplete: Bool = false // Prevent multi-threading test bugs + + testPollInfo = OpenGroupAPI.RoomPollInfo( + token: "testRoom", + activeUsers: 10, + admin: false, + globalAdmin: false, + moderator: false, + globalModerator: false, + read: false, + defaultRead: nil, + defaultAccessible: nil, + write: false, + defaultWrite: nil, + upload: false, + defaultUpload: nil, + details: TestCapabilitiesAndRoomApi.roomData.with( + moderators: [], + hiddenModerators: ["TestMod2"], + admins: [], + hiddenAdmins: [] + ) + ) + + mockStorage.write { db in + try OpenGroupManager.handlePollInfo( + db, + pollInfo: testPollInfo, + publicKey: TestConstants.publicKey, + for: "testRoom", + on: "testServer", + dependencies: dependencies + ) { didComplete = true } + } + + expect(didComplete).toEventually(beTrue(), timeout: .milliseconds(50)) + expect( + mockStorage.read { db in + try GroupMember + .filter(GroupMember.Columns.groupId == OpenGroup.idFor( + roomToken: "testRoom", + server: "testServer" + )) + .fetchOne(db) + } + ).to(equal( + GroupMember( + groupId: OpenGroup.idFor( + roomToken: "testRoom", + server: "testServer" + ), + profileId: "TestMod2", + role: .moderator, + isHidden: true ) )) } @@ -1368,7 +1434,12 @@ class OpenGroupManagerSpec: QuickSpec { defaultWrite: nil, upload: false, defaultUpload: nil, - details: TestCapabilitiesAndRoomApi.roomData.with(moderators: [], admins: ["TestAdmin"]) + details: TestCapabilitiesAndRoomApi.roomData.with( + moderators: [], + hiddenModerators: [], + admins: ["TestAdmin"], + hiddenAdmins: [] + ) ) mockStorage.write { db in @@ -1399,7 +1470,67 @@ class OpenGroupManagerSpec: QuickSpec { server: "testServer" ), profileId: "TestAdmin", - role: .admin + role: .admin, + isHidden: false + ) + )) + } + + it("updates for hidden admins") { + var didComplete: Bool = false // Prevent multi-threading test bugs + + testPollInfo = OpenGroupAPI.RoomPollInfo( + token: "testRoom", + activeUsers: 10, + admin: false, + globalAdmin: false, + moderator: false, + globalModerator: false, + read: false, + defaultRead: nil, + defaultAccessible: nil, + write: false, + defaultWrite: nil, + upload: false, + defaultUpload: nil, + details: TestCapabilitiesAndRoomApi.roomData.with( + moderators: [], + hiddenModerators: [], + admins: [], + hiddenAdmins: ["TestAdmin2"] + ) + ) + + mockStorage.write { db in + try OpenGroupManager.handlePollInfo( + db, + pollInfo: testPollInfo, + publicKey: TestConstants.publicKey, + for: "testRoom", + on: "testServer", + dependencies: dependencies + ) { didComplete = true } + } + + expect(didComplete).toEventually(beTrue(), timeout: .milliseconds(50)) + expect( + mockStorage.read { db in + try GroupMember + .filter(GroupMember.Columns.groupId == OpenGroup.idFor( + roomToken: "testRoom", + server: "testServer" + )) + .fetchOne(db) + } + ).to(equal( + GroupMember( + groupId: OpenGroup.idFor( + roomToken: "testRoom", + server: "testServer" + ), + profileId: "TestAdmin2", + role: .admin, + isHidden: true ) )) } @@ -1978,6 +2109,7 @@ class OpenGroupManagerSpec: QuickSpec { sender: nil, posted: 123, edited: nil, + deleted: nil, seqNo: 124, whisper: false, whisperMods: false, @@ -2039,6 +2171,7 @@ class OpenGroupManagerSpec: QuickSpec { sender: nil, posted: 123, edited: nil, + deleted: nil, seqNo: 124, whisper: false, whisperMods: false, @@ -2071,6 +2204,7 @@ class OpenGroupManagerSpec: QuickSpec { sender: "05\(TestConstants.publicKey)", posted: 123, edited: nil, + deleted: nil, seqNo: 124, whisper: false, whisperMods: false, @@ -2114,6 +2248,7 @@ class OpenGroupManagerSpec: QuickSpec { sender: "05\(TestConstants.publicKey)", posted: 122, edited: nil, + deleted: nil, seqNo: 123, whisper: false, whisperMods: false, @@ -2152,6 +2287,7 @@ class OpenGroupManagerSpec: QuickSpec { sender: "05\(TestConstants.publicKey)", posted: 123, edited: nil, + deleted: nil, seqNo: 123, whisper: false, whisperMods: false, @@ -2180,6 +2316,7 @@ class OpenGroupManagerSpec: QuickSpec { sender: "05\(TestConstants.publicKey)", posted: 123, edited: nil, + deleted: nil, seqNo: 123, whisper: false, whisperMods: false, @@ -2328,6 +2465,10 @@ class OpenGroupManagerSpec: QuickSpec { mockSodium .when { $0.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray()) } .thenReturn(Data(hex: testDirectMessage.sender.removingIdPrefixIfNeeded()).bytes) + + mockSodium + .when { $0.sessionId(any(), matchesBlindedId: any(), serverPublicKey: any(), genericHash: mockGenericHash) } + .thenReturn(false) } it("updates the inbox latest message id") { @@ -2422,6 +2563,10 @@ class OpenGroupManagerSpec: QuickSpec { mockSodium .when { $0.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray()) } .thenReturn(Data(hex: testDirectMessage.recipient.removingIdPrefixIfNeeded()).bytes) + + mockSodium + .when { $0.sessionId(any(), matchesBlindedId: any(), serverPublicKey: any(), genericHash: mockGenericHash) } + .thenReturn(false) } it("updates the outbox latest message id") { @@ -2602,7 +2747,8 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember( groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), profileId: "05\(TestConstants.publicKey)", - role: .moderator + role: .moderator, + isHidden: false ).insert(db) } @@ -2621,7 +2767,48 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember( groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), profileId: "05\(TestConstants.publicKey)", - role: .admin + role: .admin, + isHidden: false + ).insert(db) + } + + expect( + OpenGroupManager.isUserModeratorOrAdmin( + "05\(TestConstants.publicKey)", + for: "testRoom", + on: "testServer", + using: dependencies + ) + ).to(beTrue()) + } + + it("returns true if the moderator is hidden") { + mockStorage.write { db in + try GroupMember( + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + profileId: "05\(TestConstants.publicKey)", + role: .moderator, + isHidden: true + ).insert(db) + } + + expect( + OpenGroupManager.isUserModeratorOrAdmin( + "05\(TestConstants.publicKey)", + for: "testRoom", + on: "testServer", + using: dependencies + ) + ).to(beTrue()) + } + + it("returns true if the admin is hidden") { + mockStorage.write { db in + try GroupMember( + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + profileId: "05\(TestConstants.publicKey)", + role: .admin, + isHidden: true ).insert(db) } @@ -2672,7 +2859,8 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember( groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), profileId: "00\(otherKey)", - role: .moderator + role: .moderator, + isHidden: false ).insert(db) try Identity(variant: .ed25519PublicKey, data: Data.data(fromHex: otherKey)!).save(db) @@ -2709,7 +2897,8 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember( groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), profileId: "15\(otherKey)", - role: .moderator + role: .moderator, + isHidden: false ).insert(db) } @@ -2766,7 +2955,8 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember( groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), profileId: "05\(otherKey)", - role: .moderator + role: .moderator, + isHidden: false ).insert(db) try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: otherKey)!).save(db) @@ -2805,7 +2995,8 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember( groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), profileId: "15\(otherKey)", - role: .moderator + role: .moderator, + isHidden: false ).insert(db) try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: TestConstants.publicKey)!).save(db) @@ -2911,7 +3102,8 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember( groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), profileId: "05\(otherKey)", - role: .moderator + role: .moderator, + isHidden: false ).insert(db) try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: otherKey)!).save(db) @@ -2951,7 +3143,8 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember( groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), profileId: "00\(otherKey)", - role: .moderator + role: .moderator, + isHidden: false ).insert(db) try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: TestConstants.publicKey)!).save(db) @@ -2977,6 +3170,7 @@ class OpenGroupManagerSpec: QuickSpec { context("when getting the default rooms if needed") { beforeEach { class TestRoomsApi: TestOnionRequestAPI { + static let capabilitiesData: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: nil) static let roomsData: [OpenGroupAPI.Room] = [ TestCapabilitiesAndRoomApi.roomData, OpenGroupAPI.Room( @@ -3009,7 +3203,26 @@ class OpenGroupManagerSpec: QuickSpec { ] override class var mockResponse: Data? { - return try! JSONEncoder().encode(roomsData) + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: capabilitiesData, + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: roomsData, + failedToParseBody: false + ) + ) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) } } dependencies = dependencies.with(onionApi: TestRoomsApi.self) @@ -3178,6 +3391,7 @@ class OpenGroupManagerSpec: QuickSpec { it("fetches the image for any rooms with images") { class TestRoomsApi: TestOnionRequestAPI { + static let capabilitiesData: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: nil) static let roomsData: [OpenGroupAPI.Room] = [ OpenGroupAPI.Room( token: "test2", @@ -3209,7 +3423,26 @@ class OpenGroupManagerSpec: QuickSpec { ] override class var mockResponse: Data? { - return try! JSONEncoder().encode(roomsData) + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: capabilitiesData, + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: roomsData, + failedToParseBody: false + ) + ) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) } } let testDate: Date = Date(timeIntervalSince1970: 1234567890) @@ -3218,7 +3451,9 @@ class OpenGroupManagerSpec: QuickSpec { date: testDate ) - OpenGroupManager.getDefaultRoomsIfNeeded(using: dependencies) + OpenGroupManager + .getDefaultRoomsIfNeeded(using: dependencies) + .retainUntilComplete() expect(mockUserDefaults) .toEventually( @@ -3674,7 +3909,12 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: - Room Convenience Extensions extension OpenGroupAPI.Room { - func with(moderators: [String], admins: [String]) -> OpenGroupAPI.Room { + func with( + moderators: [String], + hiddenModerators: [String], + admins: [String], + hiddenAdmins: [String] + ) -> OpenGroupAPI.Room { return OpenGroupAPI.Room( token: self.token, name: self.name, @@ -3689,11 +3929,11 @@ extension OpenGroupAPI.Room { admin: self.admin, globalAdmin: self.globalAdmin, admins: admins, - hiddenAdmins: self.hiddenAdmins, + hiddenAdmins: hiddenAdmins, moderator: self.moderator, globalModerator: self.globalModerator, moderators: moderators, - hiddenModerators: self.hiddenModerators, + hiddenModerators: hiddenModerators, read: self.read, defaultRead: self.defaultRead, defaultAccessible: self.defaultAccessible, diff --git a/SessionUtilitiesKit/Database/Models/Identity.swift b/SessionUtilitiesKit/Database/Models/Identity.swift index cc9749ce8..9d08731f5 100644 --- a/SessionUtilitiesKit/Database/Models/Identity.swift +++ b/SessionUtilitiesKit/Database/Models/Identity.swift @@ -129,14 +129,16 @@ public extension Identity { ) } - static func fetchHexEncodedSeed() -> String? { - return Storage.shared.read { db in - guard let data: Data = try? Identity.fetchOne(db, id: .seed)?.data else { - return nil - } - - return data.toHexString() + static func fetchHexEncodedSeed(_ db: Database? = nil) -> String? { + guard let db: Database = db else { + return Storage.shared.read { db in fetchHexEncodedSeed(db) } } + + guard let data: Data = try? Identity.fetchOne(db, id: .seed)?.data else { + return nil + } + + return data.toHexString() } } diff --git a/SessionUtilitiesKit/Database/Types/TypedTableAlteration.swift b/SessionUtilitiesKit/Database/Types/TypedTableAlteration.swift new file mode 100644 index 000000000..4c21fb753 --- /dev/null +++ b/SessionUtilitiesKit/Database/Types/TypedTableAlteration.swift @@ -0,0 +1,26 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB + +/// This is a convenience wrapper around the GRDB `TableAlteration` class which allows for shorthand +/// when creating tables +public class TypedTableAlteration where T: TableRecord, T: ColumnExpressible { + let alteration: TableAlteration + + init(alteration: TableAlteration) { + self.alteration = alteration + } + + @discardableResult public func add(_ key: T.Columns, _ type: Database.ColumnType? = nil) -> ColumnDefinition { + return alteration.add(column: key.name, type) + } + + public func rename(column: String, to key: T.Columns) { + alteration.rename(column: column, to: key.name) + } + + public func drop(_ key: T.Columns) { + return alteration.drop(column: key.name) + } +} diff --git a/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift index 09a6cb7a5..278f52766 100644 --- a/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift +++ b/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift @@ -16,6 +16,17 @@ public extension Database { } } + func alter( + table: T.Type, + body: (TypedTableAlteration) -> Void + ) throws where T: TableRecord, T: ColumnExpressible { + try alter(table: T.databaseTableName) { tableAlteration in + let typedAlteration: TypedTableAlteration = TypedTableAlteration(alteration: tableAlteration) + + body(typedAlteration) + } + } + func makeFTS5Pattern(rawPattern: String, forTable table: T.Type) throws -> FTS5Pattern where T: TableRecord, T: ColumnExpressible { return try makeFTS5Pattern(rawPattern: rawPattern, forTable: table.databaseTableName) } diff --git a/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift b/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift new file mode 100644 index 000000000..135e80dc9 --- /dev/null +++ b/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift @@ -0,0 +1,105 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB + +import Quick +import Nimble + +@testable import SessionUtilitiesKit + +class IdentitySpec: QuickSpec { + // MARK: - Spec + + override func spec() { + var mockStorage: Storage! + + describe("an Identity") { + beforeEach { + mockStorage = Storage( + customWriter: DatabaseQueue(), + customMigrations: [ + SNUtilitiesKit.migrations() + ] + ) + } + + it("correctly retrieves the user user public key") { + mockStorage.write { db in + try Identity(variant: .x25519PublicKey, data: "Test1".data(using: .utf8)!).insert(db) + } + + mockStorage.read { db in + expect(Identity.fetchUserPublicKey(db)) + .to(equal("Test1".data(using: .utf8))) + } + } + + it("correctly retrieves the user private key") { + mockStorage.write { db in + try Identity(variant: .x25519PrivateKey, data: "Test2".data(using: .utf8)!).insert(db) + } + + mockStorage.read { db in + expect(Identity.fetchUserPrivateKey(db)) + .to(equal("Test2".data(using: .utf8))) + } + } + + it("correctly retrieves the user key pair") { + mockStorage.write { db in + try Identity(variant: .x25519PublicKey, data: "Test3".data(using: .utf8)!).insert(db) + try Identity(variant: .x25519PrivateKey, data: "Test4".data(using: .utf8)!).insert(db) + } + + mockStorage.read { db in + let keyPair = Identity.fetchUserKeyPair(db) + + expect(keyPair?.publicKey) + .to(equal("Test3".data(using: .utf8)?.bytes)) + expect(keyPair?.secretKey) + .to(equal("Test4".data(using: .utf8)?.bytes)) + } + } + + it("correctly determines if the user exists") { + mockStorage.write { db in + try Identity(variant: .x25519PublicKey, data: "Test3".data(using: .utf8)!).insert(db) + try Identity(variant: .x25519PrivateKey, data: "Test4".data(using: .utf8)!).insert(db) + } + + mockStorage.read { db in + expect(Identity.userExists(db)) + .to(equal(true)) + } + } + + it("correctly retrieves the user ED25519 key pair") { + mockStorage.write { db in + try Identity(variant: .ed25519PublicKey, data: "Test5".data(using: .utf8)!).insert(db) + try Identity(variant: .ed25519SecretKey, data: "Test6".data(using: .utf8)!).insert(db) + } + + mockStorage.read { db in + let keyPair = Identity.fetchUserEd25519KeyPair(db) + + expect(keyPair?.publicKey) + .to(equal("Test5".data(using: .utf8)?.bytes)) + expect(keyPair?.secretKey) + .to(equal("Test6".data(using: .utf8)?.bytes)) + } + } + + it("correctly retrieves the hex encoded seed") { + mockStorage.write { db in + try Identity(variant: .seed, data: "Test7".data(using: .utf8)!).insert(db) + } + + mockStorage.read { db in + expect(Identity.fetchHexEncodedSeed(db)) + .to(equal("5465737437")) + } + } + } + } +}