Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/src/ViewControllers/HomeView/Stories/StoryUtil.swift
1 views
//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit
import SignalUI

/// Container for util methods related to story authors.
public enum StoryUtil {

    static func authorDisplayName(
        for storyMessage: StoryMessage,
        contactsManager: any ContactManager,
        useFullNameForLocalAddress: Bool = true,
        useShortGroupName: Bool = true,
        transaction: DBReadTransaction,
    ) -> String {
        guard !storyMessage.authorAddress.isSystemStoryAddress else {
            return OWSLocalizedString(
                "SYSTEM_ADDRESS_NAME",
                comment: "Name to display for the 'system' sender, e.g. for release notes and the onboarding story",
            )
        }
        if let groupId = storyMessage.groupId {
            let groupName: String = {
                guard let groupThread = TSGroupThread.fetch(groupId: groupId, transaction: transaction) else {
                    owsFailDebug("Missing group thread for group story")
                    return TSGroupThread.defaultGroupName
                }
                return groupThread.groupNameOrDefault
            }()

            if useShortGroupName { return groupName }

            let authorShortName: String
            if storyMessage.authorAddress.isLocalAddress {
                authorShortName = CommonStrings.you
            } else {
                authorShortName = contactsManager.displayName(
                    for: storyMessage.authorAddress,
                    tx: transaction,
                ).resolvedValue(useShortNameIfAvailable: true)
            }

            let nameFormat = OWSLocalizedString(
                "GROUP_STORY_NAME_FORMAT",
                comment: "Name for a group story on the stories list. Embeds {author's name}, {group name}",
            )
            return String.localizedStringWithFormat(nameFormat, authorShortName, groupName)
        } else {
            if !useFullNameForLocalAddress, storyMessage.authorAddress.isLocalAddress {
                return CommonStrings.you
            }
            return contactsManager.displayName(
                for: storyMessage.authorAddress,
                tx: transaction,
            ).resolvedValue()
        }
    }

    /// Avatar for the _context_ of a story message. e.g. the group avatar if a group thread story, or the author's avatar otherwise.
    static func contextAvatarDataSource(
        for storyMessage: StoryMessage,
        transaction: DBReadTransaction,
    ) throws -> ConversationAvatarDataSource {
        guard let groupId = storyMessage.groupId else {
            return try authorAvatarDataSource(for: storyMessage, transaction: transaction)
        }
        guard let groupThread = TSGroupThread.fetch(groupId: groupId, transaction: transaction) else {
            throw OWSAssertionError("Missing group thread for group story")
        }
        return .thread(groupThread)
    }

    /// Avatar for the author of a story message, regardless of its context.
    static func authorAvatarDataSource(
        for storyMessage: StoryMessage,
        transaction: DBReadTransaction,
    ) throws -> ConversationAvatarDataSource {
        guard !storyMessage.authorAddress.isSystemStoryAddress else {
            return systemStoryAvatar
        }
        return .address(storyMessage.authorAddress)
    }

    private static var systemStoryAvatar: ConversationAvatarDataSource {
        return .asset(
            avatar: UIImage(named: "signal-logo-128")?
                .withTintColor(.white, renderingMode: .alwaysTemplate)
                .withBackgroundColor(.ows_accentBlue, insets: UIEdgeInsets(margin: 24)),
            badge: nil,
        )
    }

    static func askToResend(_ message: StoryMessage, in thread: TSThread, from vc: UIViewController, completion: @escaping () -> Void = {}) {
        guard !askToConfirmSafetyNumberChangesIfNecessary(for: message, from: vc) else { return }

        let title: String
        if message.hasSentToAnyRecipients {
            title = OWSLocalizedString(
                "STORY_RESEND_PARTIALLY_FAILED_MESSAGE_ACTION_SHEET",
                comment: "Title for the dialog asking user if they wish to resend a partially failed story message.",
            )
        } else {
            title = OWSLocalizedString(
                "STORY_RESEND_FAILED_MESSAGE_ACTION_SHEET",
                comment: "Title for the dialog asking user if they wish to resend a failed story message.",
            )
        }

        let actionSheet = ActionSheetController(message: title)
        actionSheet.addAction(.init(title: CommonStrings.deleteButton, style: .destructive, handler: { _ in
            SSKEnvironment.shared.databaseStorageRef.write { transaction in
                message.remotelyDelete(for: thread, transaction: transaction)
            }
            completion()
        }))
        actionSheet.addAction(.init(title: CommonStrings.sendMessage, handler: { _ in
            SSKEnvironment.shared.databaseStorageRef.write { transaction in
                message.resendMessageToFailedRecipients(transaction: transaction)
            }
            completion()
        }))
        actionSheet.addAction(.init(title: CommonStrings.cancelButton, style: .cancel, handler: { _ in
            completion()
        }))
        vc.presentActionSheet(actionSheet, animated: true)
    }

    private static func askToConfirmSafetyNumberChangesIfNecessary(for message: StoryMessage, from vc: UIViewController) -> Bool {
        let recipientsWithChangedSafetyNumber = message.failedRecipientAddresses(errorCode: UntrustedIdentityError.errorCode)
        guard !recipientsWithChangedSafetyNumber.isEmpty else { return false }

        let sheet = SafetyNumberConfirmationSheet(
            addressesToConfirm: recipientsWithChangedSafetyNumber,
            confirmationText: MessageStrings.sendButton,
        ) { confirmedSafetyNumberChange in
            guard confirmedSafetyNumberChange else { return }
            SSKEnvironment.shared.databaseStorageRef.write { transaction in
                message.resendMessageToFailedRecipients(transaction: transaction)
            }
        }
        vc.present(sheet, animated: true)

        return true
    }
}