Merge branch 'dev' into quote-standardise

This commit is contained in:
ryanzhao 2022-08-17 10:23:32 +10:00
commit 8d43eac608
35 changed files with 779 additions and 118 deletions

View File

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

View File

@ -242,6 +242,6 @@ SPEC CHECKSUMS:
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: f0857369c4831b2e5c1946345e76e493f3286805
PODFILE CHECKSUM: 0e694576fbda3c10bbc762998183d97142b85896
COCOAPODS: 1.11.3

View File

@ -644,6 +644,10 @@
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 */; };
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 */; };
@ -1683,6 +1687,10 @@
FD245C612850664300B966DD /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
FD28A4F327EA79F800FF65E7 /* BlockListUIUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockListUIUtils.swift; sourceTree = "<group>"; };
FD28A4F527EAD44C00FF65E7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_FixDeletedMessageReadState.swift; sourceTree = "<group>"; };
FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_FixHiddenModAdminSupport.swift; sourceTree = "<group>"; };
FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableAlteration.swift; sourceTree = "<group>"; };
FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentitySpec.swift; sourceTree = "<group>"; };
FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = "<group>"; };
FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequestInfoSpec.swift; sourceTree = "<group>"; };
FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponseSpec.swift; sourceTree = "<group>"; };
@ -3454,6 +3462,8 @@
FDF0B7412804EA4F004C14C5 /* _002_SetupStandardJobs.swift */,
FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */,
FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */,
FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */,
FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */,
);
path = Migrations;
sourceTree = "<group>";
@ -3520,6 +3530,7 @@
FD17D7B727F51ECA00122BE0 /* Migration.swift */,
FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */,
FD17D7C027F5200100122BE0 /* TypedTableDefinition.swift */,
FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */,
FD7162DA281B6C440060647B /* TypedTableAlias.swift */,
FD848B8A283DC509000E298B /* PagedDatabaseObserver.swift */,
);
@ -3567,6 +3578,22 @@
path = LegacyDatabase;
sourceTree = "<group>";
};
FD37EA1228AB3F60003AE748 /* Database */ = {
isa = PBXGroup;
children = (
FD37EA1328AB42C1003AE748 /* Models */,
);
path = Database;
sourceTree = "<group>";
};
FD37EA1328AB42C1003AE748 /* Models */ = {
isa = PBXGroup;
children = (
FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */,
);
path = Models;
sourceTree = "<group>";
};
FD3C905D27E410DB00CD579F /* Common Networking */ = {
isa = PBXGroup;
children = (
@ -3656,6 +3683,7 @@
FD83B9B027CF200A005E1583 /* SessionUtilitiesKitTests */ = {
isa = PBXGroup;
children = (
FD37EA1228AB3F60003AE748 /* Database */,
FD83B9B927CF20A5005E1583 /* General */,
);
path = SessionUtilitiesKitTests;
@ -5015,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 */,
@ -5122,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 */,
@ -5191,6 +5221,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 */,
@ -5440,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;
@ -6818,7 +6850,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 = 369;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -6857,7 +6889,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 2.0.0;
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 +6922,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 = 369;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -6929,7 +6961,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 2.0.0;
MARKETING_VERSION = 2.0.2;
OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,10 @@ public enum SNMessagingKit { // Just to make the external API nice
],
[
_004_RemoveLegacyYDB.self
],
[
_005_FixDeletedMessageReadState.self,
_006_FixHiddenModAdminSupport.self
]
]
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -233,9 +233,15 @@ 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)
.inserted(db)
try MessageSender.send(
db,

View File

@ -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
@ -498,13 +522,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 {

View File

@ -193,6 +193,7 @@ extension MessageReceiver {
)
)
.inserted(db)
try MessageSender
.sendNonDurably(
db,

View File

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

View File

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

View File

@ -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<SQLRequest<SessionThreadViewModel>> {
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let contact: TypedTableAlias<Contact> = TypedTableAlias()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,17 @@ public extension Database {
}
}
func alter<T>(
table: T.Type,
body: (TypedTableAlteration<T>) -> Void
) throws where T: TableRecord, T: ColumnExpressible {
try alter(table: T.databaseTableName) { tableAlteration in
let typedAlteration: TypedTableAlteration<T> = TypedTableAlteration(alteration: tableAlteration)
body(typedAlteration)
}
}
func makeFTS5Pattern<T>(rawPattern: String, forTable table: T.Type) throws -> FTS5Pattern where T: TableRecord, T: ColumnExpressible {
return try makeFTS5Pattern(rawPattern: rawPattern, forTable: table.databaseTableName)
}

View File

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

View File

@ -9,4 +9,5 @@ public enum ImageFormat {
case tiff
case jpeg
case bmp
case webp
}

View File

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

View File

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

View File

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