TM-SGNL-iOS/Signal/ConversationView/Components/CVComponentState+GroupLink.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

188 lines
7.6 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
import SignalUI
fileprivate extension CVComponentState {
private static let unfairLock = UnfairLock()
private static var groupInviteLinkAvatarCache = [String: GroupInviteLinkCachedAvatar]()
private static var groupInviteLinkAvatarsInFlight = Set<String>()
private static var expiredGroupInviteLinks = Set<URL>()
static func updateExpirationList(url: URL, isExpired: Bool) -> Bool {
unfairLock.withLock {
let alreadyExpired = expiredGroupInviteLinks.contains(url)
guard alreadyExpired != isExpired else { return false }
if isExpired {
expiredGroupInviteLinks.insert(url)
} else {
expiredGroupInviteLinks.remove(url)
}
return true
}
}
static func isGroupInviteLinkExpired(_ url: URL) -> Bool {
unfairLock.withLock {
expiredGroupInviteLinks.contains(url)
}
}
private static func cachedGroupInviteLinkAvatar(avatarUrlPath: String) -> GroupInviteLinkCachedAvatar? {
unfairLock.withLock {
guard let cachedAvatar = groupInviteLinkAvatarCache[avatarUrlPath],
cachedAvatar.isValid else {
return nil
}
return cachedAvatar
}
}
private static func loadGroupInviteLinkAvatar(avatarUrlPath: String, groupInviteLinkInfo: GroupInviteLinkInfo) -> Promise<Void> {
Self.unfairLock.withLock {
guard !groupInviteLinkAvatarsInFlight.contains(avatarUrlPath) else {
return
}
groupInviteLinkAvatarsInFlight.insert(avatarUrlPath)
}
return firstly(on: DispatchQueue.global()) { () -> Promise<Data> in
let contextInfo = try GroupV2ContextInfo.deriveFrom(masterKeyData: groupInviteLinkInfo.masterKey)
return Promise.wrapAsync {
try await SSKEnvironment.shared.groupsV2Ref.fetchGroupInviteLinkAvatar(
avatarUrlPath: avatarUrlPath,
groupSecretParams: contextInfo.groupSecretParams
)
}
}.map(on: DispatchQueue.global()) { (avatarData: Data) -> Void in
let imageMetadata = avatarData.imageMetadata(withPath: nil, mimeType: nil)
let cacheFileUrl = OWSFileSystem.temporaryFileUrl(fileExtension: imageMetadata.fileExtension, isAvailableWhileDeviceLocked: true)
guard imageMetadata.isValid else {
let cachedAvatar = GroupInviteLinkCachedAvatar(
cacheFileUrl: cacheFileUrl,
imageSizePixels: imageMetadata.pixelSize,
isValid: false
)
Self.unfairLock.withLock {
Self.groupInviteLinkAvatarCache[avatarUrlPath] = cachedAvatar
Self.groupInviteLinkAvatarsInFlight.remove(avatarUrlPath)
}
throw OWSAssertionError("Invalid group avatar.")
}
try avatarData.write(to: cacheFileUrl)
let cachedAvatar = GroupInviteLinkCachedAvatar(
cacheFileUrl: cacheFileUrl,
imageSizePixels: imageMetadata.pixelSize,
isValid: true
)
Self.unfairLock.withLock {
Self.groupInviteLinkAvatarCache[avatarUrlPath] = cachedAvatar
Self.groupInviteLinkAvatarsInFlight.remove(avatarUrlPath)
}
}.recover(on: DispatchQueue.global()) { (error) -> Promise<Void> in
_ = Self.unfairLock.withLock {
Self.groupInviteLinkAvatarsInFlight.remove(avatarUrlPath)
}
throw error
}
}
}
// MARK: -
extension CVComponentState {
// MARK: - Notifications
static func configureGroupInviteLink(
_ url: URL,
message: TSMessage,
groupInviteLinkInfo: GroupInviteLinkInfo
) -> GroupInviteLinkViewModel {
let touchMessage = {
SSKEnvironment.shared.databaseStorageRef.write { transaction in
SSKEnvironment.shared.databaseStorageRef.touch(interaction: message, shouldReindex: false, transaction: 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.
firstly(on: DispatchQueue.global()) { () -> Promise<GroupInviteLinkPreview> in
let groupContextInfo = try GroupV2ContextInfo.deriveFrom(masterKeyData: groupInviteLinkInfo.masterKey)
return Promise.wrapAsync {
try await SSKEnvironment.shared.groupsV2Ref.fetchGroupInviteLinkPreview(
inviteLinkPassword: groupInviteLinkInfo.inviteLinkPassword,
groupSecretParams: groupContextInfo.groupSecretParams,
allowCached: false
)
}
}.done(on: DispatchQueue.global()) { (_: GroupInviteLinkPreview) in
_ = Self.updateExpirationList(url: url, isExpired: false)
touchMessage()
}.catch(on: DispatchQueue.global()) { (error: Error) in
switch error {
case GroupsV2Error.expiredGroupInviteLink, GroupsV2Error.localUserBlockedFromJoining:
Logger.warn("Failed to fetch group link content: \(error)")
if Self.updateExpirationList(url: url, isExpired: true) {
touchMessage()
}
default:
// TODO: Add retry?
owsFailDebugUnlessNetworkFailure(error)
}
}
return GroupInviteLinkViewModel(
url: url,
groupInviteLinkPreview: nil,
avatar: nil,
isExpired: Self.isGroupInviteLinkExpired(url)
)
}
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.
firstly(on: DispatchQueue.global()) {
Self.loadGroupInviteLinkAvatar(avatarUrlPath: avatarUrlPath, groupInviteLinkInfo: groupInviteLinkInfo)
}.done(on: DispatchQueue.global()) { () in
touchMessage()
}.catch { error in
// 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
)
}
}