Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/Calls/UserInterface/CallLinkApprovalRequestView.swift
1 views
//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import LibSignalClient
import SignalRingRTC
import SignalServiceKit
import SignalUI
import SwiftUI

private typealias ApprovalRequest = CallLinkApprovalRequest

// MARK: - ApprovalRequestView

private struct ApprovalRequestView: View {
    enum Requests {
        case single(ApprovalRequest)
        case many(
            topRequest: ApprovalRequest,
            amountMore: Int,
            didTapMore: () -> Void,
        )
    }

    var requests: Requests
    var openProfileDetails: () -> Void
    var didApprove: () -> Void
    var didDeny: () -> Void

    private var request: ApprovalRequest {
        switch requests {
        case let .single(approvalRequest):
            approvalRequest
        case let .many(topRequest, _, _):
            topRequest
        }
    }

    // MARK: Body

    var body: some View {
        VStack {
            HStack(spacing: 8) {
                self.profileDetailsButton {
                    AvatarView(
                        dataSource: .address(self.request.address),
                        sizeClass: .fortyEight,
                        localUserDisplayMode: .asLocalUser,
                        badged: true,
                    )
                }

                VStack(alignment: .leading) {
                    self.profileDetailsButton {
                        HStack {
                            Text("\(self.request.name)")
                                .lineLimit(1)
                            Text("\(Image(systemName: "chevron.forward"))")
                                // Per design, we want a new chevron that fades
                                // in/out with the new name instead of the same one
                                // sliding around as the name length changes.
                                .id(self.request.name)
                        }
                        .font(.body.bold())
                    }

                    self.profileDetailsButton {
                        Text(
                            OWSLocalizedString(
                                "CALL_LINK_JOIN_REQUEST_PROMPT",
                                comment: "Tappable label that appears below a user's name on a toast when they request to join a call link.",
                            ),
                        )
                        .multilineTextAlignment(.leading)
                        .font(.subheadline)
                    }
                }

                Spacer()

                HStack(spacing: 20) {
                    ActionButton(action: self.didDeny, color: .red, systemImage: "xmark")
                    ActionButton(action: self.didApprove, color: .green, systemImage: "checkmark")
                }
            }
            .padding(12)

            switch requests {
            case .single:
                EmptyView()
            case let .many(_, amountMore, action):
                MoreRequestsView(amountMore: amountMore, action: action)
            }
        }
        .foregroundColor(.white)
        .background(.regularMaterial)
        .environment(\.colorScheme, .dark)
        .cornerRadius(10)
    }

    // MARK: Helper views

    private func profileDetailsButton(label: () -> some View) -> some View {
        Button(action: self.openProfileDetails, label: label)
    }

    struct ActionButton: View {
        var action: () -> Void
        var color: Color
        var systemImage: String

        var body: some View {
            Button(action: self.action) {
                Circle()
                    .frame(width: 36, height: 36)
                    .foregroundColor(self.color)
                    .overlay {
                        Image(systemName: self.systemImage)
                            .resizable()
                            .font(.body.bold())
                            .padding(11)
                    }
            }
        }
    }

    struct MoreRequestsView: View {
        var amountMore: Int
        var action: () -> Void

        var body: some View {
            Button(action: self.action) {
                Group {
                    if #available(iOS 16.0, *) {
                        Text(CallStrings.callLinkMoreRequests(count: self.amountMore))
                            .contentTransition(.numericText())
                    } else {
                        Text(CallStrings.callLinkMoreRequests(count: self.amountMore))
                    }
                }
                .padding(.horizontal, 16)
                .padding(.vertical, 6)
                .background {
                    Capsule()
                        .fill(Color(.ows_gray65))
                }
            }
            .font(.subheadline.weight(.medium))
            .padding(12)
            .frame(maxWidth: .infinity)
            .background(.thinMaterial)
            .environment(\.colorScheme, .dark)
        }
    }
}

// MARK: - ApprovalRequestStack

struct ApprovalRequestStack: View {
    typealias ApprovalRequest = CallLinkApprovalRequest

    @ObservedObject var viewModel: CallLinkApprovalViewModel

    var didTapMore: ([ApprovalRequest]) -> Void
    var didChangeHeight: (CGFloat) -> Void

    private var cappedRequests: [ApprovalRequest] {
        if self.viewModel.requests.count > 2 {
            Array(self.viewModel.requests.prefix(1))
        } else {
            self.viewModel.requests
        }
    }

    // MARK: Body

    var body: some View {
        ZStack(alignment: .bottom) {
            let displayedRequests = self.cappedRequests

            ForEach(displayedRequests) { request in
                let isTopOfStack = displayedRequests.count == 2 && request == displayedRequests.first
                let isBottomOfStack = displayedRequests.count == 2 && request == displayedRequests.last
                let isOnlyRequest = self.viewModel.requests.count == 1
                let requests: ApprovalRequestView.Requests =
                    if self.viewModel.requests.count <= 2 {
                        .single(request)
                    } else {
                        .many(
                            topRequest: request,
                            amountMore: self.viewModel.requests.count - 1,
                            didTapMore: {
                                self.didTapMore(self.viewModel.requests)
                            },
                        )
                    }

                ApprovalRequestView(
                    requests: requests,
                    openProfileDetails: { self.viewModel.performRequestAction.send((.viewDetails, request)) },
                    didApprove: { self.viewModel.performRequestAction.send((.approve, request)) },
                    didDeny: { self.viewModel.performRequestAction.send((.deny, request)) },
                )
                .zIndex({
                    if isTopOfStack {
                        10
                    } else if isOnlyRequest {
                        -10
                    } else {
                        0
                    }
                }())
                .transition(.asymmetric(
                    insertion: isOnlyRequest ? .offset(y: 12).combined(with: .opacity) : .offset(y: -10).combined(with: .opacity),
                    removal: .move(edge: .top).combined(with: .opacity),
                ))
                .padding(.bottom, isTopOfStack ? 12 : 0)
                .scaleEffect(isBottomOfStack ? .init(width: 0.95, height: 0.95) : .init(width: 1, height: 1), anchor: .bottom)
            }
        }
        .background {
            GeometryReader { geometry in
                EmptyView()
                    .onAppear {
                        self.didChangeHeight(geometry.size.height)
                    }
                    .onChange(of: geometry.size.height) { newValue in
                        self.didChangeHeight(newValue)
                    }
            }
        }
    }
}

// MARK: Preview

#if DEBUG

private extension ApprovalRequest {
    static let candice = ApprovalRequest(aci: .init(fromUUID: UUID()), name: "Candice")
    static let kai = ApprovalRequest(aci: .init(fromUUID: UUID()), name: "Kai really long first name")
    static let gerte = ApprovalRequest(aci: .init(fromUUID: UUID()), name: "Gerte")
    static let sam = ApprovalRequest(aci: .init(fromUUID: UUID()), name: "Sam")
}

private struct PreviewView: View {
    static let startingRequests: [ApprovalRequest] = [
        .candice,
        .kai,
    ]

    @StateObject private var viewModel = CallLinkApprovalViewModel()
    private var requests: [ApprovalRequest] { viewModel.requests }
    @State private var height: CGFloat = 0

    private func personToAdd() -> ApprovalRequest {
        if !requests.contains(.candice) {
            .candice
        } else if !requests.contains(.kai) {
            .kai
        } else if !requests.contains(.gerte) {
            .gerte
        } else {
            .sam
        }
    }

    private var addPersonButtonText: String? {
        switch requests.count {
        case ...0:
            "Add request"
        case 1:
            "Add to bottom of stack"
        case 2, 3:
            "Add more"
        default:
            nil
        }
    }

    var body: some View {
        VStack {
            VStack {
                Text("\(self.height)")

                Button(String("Reset")) {
                    withAnimation(.quickSpring()) {
                        self.viewModel.requests = Self.startingRequests
                    }
                }

                if let addPersonButtonText {
                    Button(addPersonButtonText) {
                        withAnimation(.quickSpring()) {
                            self.viewModel.requests.append(personToAdd())
                        }
                    }
                    .animation(.none, value: requests)
                }
            }
            .frame(maxWidth: .infinity)
            .onAppear {
                self.viewModel.requests = Self.startingRequests
            }

            Spacer()

            ApprovalRequestStack(
                viewModel: self.viewModel,
                didTapMore: { _ in },
                didChangeHeight: { height in self.height = height },
            )
        }
    }

    private func didTapButton(_ request: ApprovalRequest) {
        if requests.count > 2 {
            let firstItemID = requests[0].id
            var newRequests = requests
            newRequests.removeFirst()
            newRequests[0].id = firstItemID
            withAnimation(.quickSpring()) {
                self.viewModel.requests = newRequests
            }
        } else {
            withAnimation(.quickSpring()) {
                self.viewModel.requests.removeAll { $0 == request }
            }
        }
    }
}

#Preview() {
    PreviewView()
        .padding()
}

#endif