// // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation import LibSignalClient import SignalServiceKit extension TSInfoMessage.PersistableGroupUpdateItem { static func cvComponentAction( items: [Self], groupThread: () -> TSGroupThread?, contactsManager: any ContactManager, tx: SDSAnyReadTransaction ) -> CVComponentSystemMessage.Action? { guard !items.isEmpty else { return nil } // Cache the group thread so we only fetch it once. var hasFetchedGroupThread = false var cachedGroupThread: TSGroupThread? let cachingGroupThread: () -> TSGroupThread? = { if hasFetchedGroupThread { return cachedGroupThread } else { cachedGroupThread = groupThread() hasFetchedGroupThread = true return cachedGroupThread } } var index = 0 while index < items.count { let item = items[index] defer { index += 1 } /// Normally we use the action from the first non-nil item /// on the info message. /// It is legal, in a backup, to have a single TSInfoMessage /// with both a collapsed .sequenceOfInviteLinkRequestAndCancels /// and a single request to join right after. (Which implies the /// sequence ends in a cancel, and a new request came after). /// In this case we want to show the action from the request to join /// (an "accept request" item) that follows, so do a little lookahead /// to catch this exception case. if case let .sequenceOfInviteLinkRequestAndCancels(_, _, isTail) = item, let nextItem = items[safe: index + 1], nextItem.representsCollapsibleSingleRequestToJoin() != nil, let nextItemAction = item.cvComponentAction( groupThread: cachingGroupThread, contactsManager: contactsManager, tx: tx ) { owsAssertDebug( isTail.negated, "Collapsed item with a following request shouldn't be a tail!" ) return nextItemAction } if let action = item.cvComponentAction( groupThread: cachingGroupThread, contactsManager: contactsManager, tx: tx ) { return action } } return nil } private func cvComponentAction( groupThread: () -> TSGroupThread?, contactsManager: any ContactManager, tx: SDSAnyReadTransaction ) -> CVComponentSystemMessage.Action? { typealias Action = CVComponentSystemMessage.Action switch self { case let .sequenceOfInviteLinkRequestAndCancels(requester, count, isTail): if count == 0 { // This is just a request to join. return Action.forNewlyRequestingMembers(count: 1) } return Action.sequenceOfInviteLinkRequestAndCancelsAction( requester: requester.wrappedValue, isTail: isTail, groupThread: groupThread, contactsManager: contactsManager, tx: tx ) case .inviteFriendsToNewlyCreatedGroup: // We should use the latest group model, not the one from the time // the info message was made. guard let thread = groupThread() else { return nil } return Action( title: OWSLocalizedString( "GROUPS_INVITE_FRIENDS_BUTTON", comment: "Label for 'invite friends to group' button." ), accessibilityIdentifier: "group_invite_friends", action: .didTapGroupInviteLinkPromotion(groupModel: thread.groupModel) ) case .wasMigrated: return Action( title: CommonStrings.learnMore, accessibilityIdentifier: "group_migration_learn_more", action: .didTapGroupMigrationLearnMore ) case .descriptionChangedByLocalUser(let newGroupDescription), .descriptionChangedByOtherUser(_, let newGroupDescription), .descriptionChangedByUnknownUser(let newGroupDescription): return Action( title: CommonStrings.viewButton, accessibilityIdentifier: "group_description_view", action: .didTapViewGroupDescription(newGroupDescription: newGroupDescription) ) case let .unnamedUserInvitesWereRevokedByLocalUser(count), let .unnamedUsersWereInvitedByOtherUser(_, count), let .unnamedUsersWereInvitedByUnknownUser(count): return Action.forNewlyRequestingMembers(count: count) case .localUserRequestedToJoin, .otherUserRequestedToJoin: return Action.forNewlyRequestingMembers(count: 1) default: return nil } } } fileprivate extension CVComponentSystemMessage.Action { static func sequenceOfInviteLinkRequestAndCancelsAction( requester: Aci, isTail: Bool, groupThread: () -> TSGroupThread?, contactsManager: any ContactManager, tx: SDSAnyReadTransaction ) -> Self? { guard isTail else { return nil } guard let mostRecentGroupModel = groupThread()?.groupModel as? TSGroupModelV2 else { owsFailDebug("Missing group thread for join request sequence") return nil } // Only show the option to ban if we are an admin, and they are // not already banned. We want to use the most up-to-date group // model here instead of the one on the info message, since // group state may have changed since that message. guard mostRecentGroupModel.groupMembership.isLocalUserFullMemberAndAdministrator, !mostRecentGroupModel.groupMembership.isBannedMember(requester) else { return nil } return CVComponentSystemMessage.Action( title: OWSLocalizedString( "GROUPS_BLOCK_REQUEST_BUTTON", comment: "Label for button that lets the user block a request to join the group." ), accessibilityIdentifier: "block_join_request_button", action: .didTapBlockRequest( groupModel: mostRecentGroupModel, requesterName: contactsManager.displayName( for: SignalServiceAddress(requester), tx: tx ).resolvedValue(useShortNameIfAvailable: true), requesterAci: requester ) ) } static func forNewlyRequestingMembers(count: UInt) -> Self { let title: String = { if count > 1 { return OWSLocalizedString( "GROUPS_VIEW_REQUESTS_BUTTON", comment: "Label for button that lets the user view the requests to join the group." ) } else { return OWSLocalizedString( "GROUPS_VIEW_REQUEST_BUTTON", comment: "Label for button that lets the user view the request to join the group." ) } }() return Self( title: title, accessibilityIdentifier: "show_group_requests_button", action: .didTapShowConversationSettingsAndShowMemberRequests ) } }