TM-SGNL-iOS/Signal/Calls/UserInterface/CallLinkApprovalRequestView.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

339 lines
11 KiB
Swift

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SwiftUI
import SignalUI
import SignalRingRTC
import LibSignalClient
import SignalServiceKit
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