Merge branch 'dev' into quote-standardise
This commit is contained in:
commit
8d43eac608
3
Podfile
3
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
|
||||
|
||||
|
|
|
@ -242,6 +242,6 @@ SPEC CHECKSUMS:
|
|||
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
|
||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||
|
||||
PODFILE CHECKSUM: f0857369c4831b2e5c1946345e76e493f3286805
|
||||
PODFILE CHECKSUM: 0e694576fbda3c10bbc762998183d97142b85896
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 ||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,10 @@ public enum SNMessagingKit { // Just to make the external API nice
|
|||
],
|
||||
[
|
||||
_004_RemoveLegacyYDB.self
|
||||
],
|
||||
[
|
||||
_005_FixDeletedMessageReadState.self,
|
||||
_006_FixHiddenModAdminSupport.self
|
||||
]
|
||||
]
|
||||
)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -193,6 +193,7 @@ extension MessageReceiver {
|
|||
)
|
||||
)
|
||||
.inserted(db)
|
||||
|
||||
try MessageSender
|
||||
.sendNonDurably(
|
||||
db,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)!
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,4 +9,5 @@ public enum ImageFormat {
|
|||
case tiff
|
||||
case jpeg
|
||||
case bmp
|
||||
case webp
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue