Path: blob/main/Signal/ConversationView/Components/CVComponentState+GroupLink.swift
1 views
//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
import SignalUI
private extension CVComponentState {
private struct GroupLinkState {
var groupInviteLinkAvatarCache = [String: GroupInviteLinkCachedAvatar]()
var expiredGroupInviteLinks = Set<GroupInviteLinkInfo>()
}
private static let groupLinkState = AtomicValue(GroupLinkState(), lock: .init())
private static func updateExpirationList(groupInviteLinkInfo: GroupInviteLinkInfo, isExpired: Bool) -> Bool {
return groupLinkState.update {
if isExpired {
return $0.expiredGroupInviteLinks.insert(groupInviteLinkInfo).inserted
} else {
return $0.expiredGroupInviteLinks.remove(groupInviteLinkInfo) != nil
}
}
}
private static func isGroupInviteLinkExpired(groupInviteLinkInfo: GroupInviteLinkInfo) -> Bool {
return groupLinkState.update {
return $0.expiredGroupInviteLinks.contains(groupInviteLinkInfo)
}
}
private static func cachedGroupInviteLinkAvatar(avatarUrlPath: String) -> GroupInviteLinkCachedAvatar? {
return groupLinkState.update {
guard let cachedAvatar = $0.groupInviteLinkAvatarCache[avatarUrlPath], cachedAvatar.isValid else {
return nil
}
return cachedAvatar
}
}
private static func loadGroupInviteLinkAvatar(avatarUrlPath: String, groupInviteLinkInfo: GroupInviteLinkInfo) async throws {
let contextInfo = try GroupV2ContextInfo.deriveFrom(masterKeyData: groupInviteLinkInfo.masterKey)
let avatarData = try await SSKEnvironment.shared.groupsV2Ref.fetchGroupInviteLinkAvatar(
avatarUrlPath: avatarUrlPath,
groupSecretParams: contextInfo.groupSecretParams,
)
let imageMetadata = DataImageSource(avatarData).imageMetadata()
guard let imageMetadata else {
let cachedAvatar = GroupInviteLinkCachedAvatar(
cacheFileUrl: OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: true,
),
imageSizePixels: .zero,
isValid: false,
)
groupLinkState.update {
$0.groupInviteLinkAvatarCache[avatarUrlPath] = cachedAvatar
}
throw OWSAssertionError("Invalid group avatar.")
}
let cacheFileUrl = OWSFileSystem.temporaryFileUrl(
fileExtension: imageMetadata.imageFormat.fileExtension,
isAvailableWhileDeviceLocked: true,
)
try avatarData.write(to: cacheFileUrl)
let cachedAvatar = GroupInviteLinkCachedAvatar(
cacheFileUrl: cacheFileUrl,
imageSizePixels: imageMetadata.pixelSize,
isValid: true,
)
groupLinkState.update {
$0.groupInviteLinkAvatarCache[avatarUrlPath] = cachedAvatar
}
}
}
// MARK: -
extension CVComponentState {
// MARK: - Notifications
static func configureGroupInviteLink(
_ url: URL,
message: TSMessage,
groupInviteLinkInfo: GroupInviteLinkInfo,
) -> GroupInviteLinkViewModel {
let touchMessage = { () async -> Void in
await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { transaction in
SSKEnvironment.shared.databaseStorageRef.touch(interaction: message, shouldReindex: false, tx: transaction)
}
}
guard let groupInviteLinkPreview = GroupManager.cachedGroupInviteLinkPreview(groupInviteLinkInfo: groupInviteLinkInfo) else {
// If there is no cached GroupInviteLinkPreview for this link,
// try to do load it now. On success, touch the interaction
// in order to trigger reload of the view.
Task {
do {
let groupContextInfo = try GroupV2ContextInfo.deriveFrom(masterKeyData: groupInviteLinkInfo.masterKey)
_ = try await SSKEnvironment.shared.groupsV2Ref.fetchGroupInviteLinkPreview(
inviteLinkPassword: groupInviteLinkInfo.inviteLinkPassword,
groupSecretParams: groupContextInfo.groupSecretParams,
)
_ = Self.updateExpirationList(groupInviteLinkInfo: groupInviteLinkInfo, isExpired: false)
await touchMessage()
} catch {
switch error {
case GroupsV2Error.expiredGroupInviteLink, GroupsV2Error.localUserBlockedFromJoining, GroupsV2Error.terminatedGroupInviteLink:
Logger.warn("Failed to fetch group link content: \(error)")
if Self.updateExpirationList(groupInviteLinkInfo: groupInviteLinkInfo, isExpired: true) {
await touchMessage()
}
default:
// TODO: Add retry?
owsFailDebugUnlessNetworkFailure(error)
}
}
}
return GroupInviteLinkViewModel(
url: url,
groupInviteLinkPreview: nil,
avatar: nil,
isExpired: Self.isGroupInviteLinkExpired(groupInviteLinkInfo: groupInviteLinkInfo),
)
}
guard let avatarUrlPath = groupInviteLinkPreview.avatarUrlPath else {
// If this group link has no avatar, there's nothing left to load.
return GroupInviteLinkViewModel(
url: url,
groupInviteLinkPreview: groupInviteLinkPreview,
avatar: nil,
isExpired: false,
)
}
guard let avatar = Self.cachedGroupInviteLinkAvatar(avatarUrlPath: avatarUrlPath) else {
// If there is no cached avatar for this link, try to do load it now. On
// success, touch the interaction in order to trigger reload of the view.
Task {
do {
try await Self.loadGroupInviteLinkAvatar(avatarUrlPath: avatarUrlPath, groupInviteLinkInfo: groupInviteLinkInfo)
await touchMessage()
} catch {
// TODO: Add retry?
owsFailDebugUnlessNetworkFailure(error)
}
}
return GroupInviteLinkViewModel(
url: url,
groupInviteLinkPreview: groupInviteLinkPreview,
avatar: nil,
isExpired: false,
)
}
return GroupInviteLinkViewModel(
url: url,
groupInviteLinkPreview: groupInviteLinkPreview,
avatar: avatar,
isExpired: false,
)
}
}