Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Flipcash/Core/Screens/Conversation/ConversationScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -733,9 +733,9 @@ struct ConversationScreen: View {
.sheet(isPresented: $isShowingEncryptionInfo) {
E2eeLearnMoreSheet(kind: .dm, isPresented: $isShowingEncryptionInfo)
}
.sheet(isPresented: $isInviting) {
.fullScreenCover(isPresented: $isInviting) {
if let conversationID {
GroupInviteSheet(conversationID: conversationID, isPresented: $isInviting) { chatID in
ShareToChatsSheet(subject: .group(conversationID), isPresented: $isInviting) { chatID in
router.push(.tipConversation(chatID))
}
}
Expand Down
4 changes: 3 additions & 1 deletion Flipcash/Core/Screens/Main/Tips/ChatActionRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ struct ChatActionRow: View {

let icon: Image
let title: String
/// The card's fill: ``Color/backgroundRow`` over the main background, lighter on a lifted sheet.
var background: Color = .backgroundRow
let accessibilityIdentifier: String
let action: () -> Void

Expand All @@ -36,7 +38,7 @@ struct ChatActionRow: View {
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.backgroundRow, in: .rect(cornerRadius: Metrics.buttonRadius))
.background(background, in: .rect(cornerRadius: Metrics.buttonRadius))
.contentShape(.rect(cornerRadius: Metrics.buttonRadius))
}
.buttonStyle(.plain)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// GroupInviteSheet.swift
// ShareToChatsSheet.swift
// Flipcash
//

Expand All @@ -8,14 +8,22 @@ import UIKit
import FlipcashCore
import FlipcashUI

/// "Invite People" (nodes 10330:19387, 10330:19549, 10329:12104): hand out a group's invite link by
/// Share or Copy, or post it straight into recent chats with an optional message.
///
/// A group is not discoverable — the link built from its id is the only way in — so every path here
/// sends the same URL.
struct GroupInviteSheet: View {
/// What ``ShareToChatsSheet`` hands out.
enum ShareToChatsSubject {
/// A group's invite link. A group is not discoverable — the link built from its id is the only
/// way in.
case group(ConversationID)
/// A person's public profile link, the one their own You tab shares. `preview` is the tip-code
/// image for the system share sheet's card, when one is rendered. `directChatID` is the viewer's
/// DM with them, left out of the picker so nobody is sent their own profile.
case user(url: URL, displayName: String?, preview: TipCodePreview? = nil, directChatID: ConversationID?)
}

/// "Invite People" (nodes 10330:19387, 10330:19549, 10329:12104): hand out a link by Share or Copy,
/// or post it straight into recent chats with an optional message.
struct ShareToChatsSheet: View {

let conversationID: ConversationID
let subject: ShareToChatsSubject

@Binding var isPresented: Bool

Expand All @@ -37,11 +45,52 @@ struct GroupInviteSheet: View {
@State private var headerHeight: CGFloat = 0
@FocusState private var isMessageFocused: Bool

init(conversationID: ConversationID, isPresented: Binding<Bool>, onInvited: @escaping (ConversationID) -> Void) {
self.conversationID = conversationID
init(subject: ShareToChatsSubject, isPresented: Binding<Bool>, onInvited: @escaping (ConversationID) -> Void) {
self.subject = subject
self._isPresented = isPresented
self.onInvited = onInvited
self._model = State(initialValue: InvitePeopleViewModel(url: .groupChatInvite(for: conversationID)))
let url: URL = switch subject {
case .group(let id): .groupChatInvite(for: id)
case .user(let url, _, _, _): url
}
self._model = State(initialValue: InvitePeopleViewModel(url: url))
}

/// The group being invited to, nil when sharing a person.
private var groupID: ConversationID? {
switch subject {
case .group(let id): id
case .user: nil
}
}

/// The chat the picker leaves out: the group itself, or the DM with the person shared.
private var excludedChatID: ConversationID? {
switch subject {
case .group(let id): id
case .user(_, _, _, let directChatID): directChatID
}
}

private var sheetTitle: String {
switch subject {
case .group: "Invite People"
case .user: "Share Profile"
}
}

private var copyTitle: String {
switch subject {
case .group: "Copy Invite Link"
case .user: "Copy Link"
}
}

private var sendTitle: String {
switch subject {
case .group: "Invite"
case .user: "Send"
}
}

/// The group's own title, which names it in the shared message.
Expand All @@ -55,13 +104,13 @@ struct GroupInviteSheet: View {
}

private var conversation: Conversation? {
conversationController.conversation(withID: conversationID)
groupID.flatMap(conversationController.conversation(withID:))
}

/// The chats the Chats tab lists, newest activity first: 1:1 chats and joined groups, less the
/// group being invited to. Search and people outside recent chats are out of scope.
/// The chats the Chats tab lists, newest activity first: 1:1 chats and joined groups, less
/// ``excludedChatID``. Search and people outside recent chats are out of scope.
private var recentChats: [Conversation] {
conversationController.chatListConversations.filter { $0.id != conversationID }
conversationController.chatListConversations.filter { $0.id != excludedChatID }
}

/// The group's picture for the share sheet's own preview card.
Expand All @@ -70,11 +119,21 @@ struct GroupInviteSheet: View {
/// every render of a sheet that shows it nowhere. Nil while the avatar hasn't loaded or the
/// group has none — the card then carries the name alone.
private func icon() -> UIImage? {
sessionContainer.profileAvatars
.data(for: .chat(conversationID))
groupID
.flatMap { sessionContainer.profileAvatars.data(for: .chat($0)) }
.flatMap(UIImage.init(data:))
}

/// What Share hands the system share sheet: the group's preview card, or the person's.
private func shareItem() -> UIActivityItemSource {
switch subject {
case .group:
GroupInviteShareItem(url: model.url, title: title, icon: icon())
case .user(let url, let displayName, let preview, _):
TipCodeShareItem.profile(url: url, displayName: displayName, preview: preview)
}
}

var body: some View {
Background(color: .backgroundMain) {
VStack(spacing: 0) {
Expand Down Expand Up @@ -136,21 +195,21 @@ struct GroupInviteSheet: View {
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
.presentationDetents([.large])
.presentationBackground(Color.backgroundMain)
.interactiveDismissDisabled(model.isSending)
.task {
// Idempotent: the store returns without a round trip once the bytes are in memory or
// on disk. Loading here rather than relying on the presenting screen keeps the preview
// card's icon independent of which screen opened the sheet.
await sessionContainer.profileAvatars.load(.chat(conversationID), picture: conversation?.picture)
guard let groupID else { return }
await sessionContainer.profileAvatars.load(.chat(groupID), picture: conversation?.picture)
}
}

/// The sheet's own title bar: a centred title and a glass close button on the right.
private var header: some View {
ZStack {
Text("Invite People")
Text(sheetTitle)
.font(.appBarButton)
.foregroundStyle(Color.textMain)

Expand All @@ -173,14 +232,12 @@ struct GroupInviteSheet: View {
.resizable()
.scaledToFit()
} action: {
Analytics.groupInviteShared(method: .share)
ShareSheet.present(
activityItem: GroupInviteShareItem(url: model.url, title: title, icon: icon())
) { _ in }
if groupID != nil { Analytics.groupInviteShared(method: .share) }
ShareSheet.present(activityItem: shareItem()) { _ in }
}
.accessibilityIdentifier("group-invite-send")

ShareTile(title: didCopy ? "Copied" : "Copy Invite Link") {
ShareTile(title: didCopy ? "Copied" : copyTitle) {
if didCopy {
Image.system(.circleCheck)
.resizable()
Expand Down Expand Up @@ -230,7 +287,7 @@ struct GroupInviteSheet: View {

Button(action: invite) {
ZStack {
Text("Invite").opacity(model.isSending ? 0 : 1)
Text(sendTitle).opacity(model.isSending ? 0 : 1)
if model.isSending {
ProgressView().tint(Color.textAction)
}
Expand Down Expand Up @@ -272,7 +329,7 @@ struct GroupInviteSheet: View {
.frame(maxWidth: .infinity, alignment: .leading)
.frame(minHeight: BarMetrics.fieldMinHeight)

Text("Invite")
Text(sendTitle)
.font(.default(size: 17, weight: .medium))
.padding(.horizontal, 14)
.frame(height: BarMetrics.fieldMinHeight)
Expand Down Expand Up @@ -301,7 +358,7 @@ struct GroupInviteSheet: View {
}

private func copy() {
Analytics.groupInviteShared(method: .theCopy)
if groupID != nil { Analytics.groupInviteShared(method: .theCopy) }
UIPasteboard.general.string = model.url.absoluteString
withAnimation(.easeInOut(duration: 0.15)) {
didCopy = true
Expand Down Expand Up @@ -338,7 +395,7 @@ private struct ShareTile<Icon: View>: View {
.padding(.vertical, 16)
.padding(.horizontal, 8)
.frame(maxWidth: .infinity, minHeight: 95)
.background(Color.white.opacity(0.05))
.background(Color.backgroundRow)
.clipShape(RoundedRectangle(cornerRadius: Metrics.buttonRadius, style: .continuous))
.contentShape(RoundedRectangle(cornerRadius: Metrics.buttonRadius, style: .continuous))
}
Expand Down
39 changes: 16 additions & 23 deletions Flipcash/Core/Screens/Main/You/YouScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ struct YouScreen: View {
@Environment(SessionContainer.self) private var sessionContainer
@Environment(AppRouter.self) private var router
@Environment(RatesController.self) private var ratesController
@Environment(ToastController.self) private var toasts

/// Warms the share-sheet preview image ahead of the share tap so it never
/// lands on the tap; keyed by user.
Expand All @@ -35,6 +34,7 @@ struct YouScreen: View {

@State private var isShowingShare = false
@State private var shareChoice: ProfileShareChoice?
@State private var isSharingToChats = false
@State private var isShowingCardDownload = false

/// The gap the page keeps between its last row and the tab bar.
Expand Down Expand Up @@ -116,7 +116,20 @@ struct YouScreen: View {
}
.dialog(item: $usernameDialog)
.sheet(isPresented: $isShowingShare, onDismiss: handleShareChoice) {
ProfileShareSheet(subtitle: shareSubtitle, offersCard: true) { shareChoice = $0 }
ProfileShareSheet { shareChoice = $0 }
}
.fullScreenCover(isPresented: $isSharingToChats) {
ShareToChatsSheet(
subject: .user(
url: url,
displayName: displayName,
preview: previewCache.preview(for: sessionContainer.session.userID),
directChatID: nil
),
isPresented: $isSharingToChats
) { chatID in
router.push(.tipConversation(chatID))
}
}
}

Expand Down Expand Up @@ -312,24 +325,10 @@ struct YouScreen: View {
TipCode.Payload(userID: sessionContainer.session.userID).codeData()
}

private var shareSubtitle: String? {
guard let displayName else { return nil }
return [displayName, username?.handle].compactMap { $0 }.joined(separator: " · ")
}

private var url: URL { .tipcard(for: sessionContainer.session.userID, username: username) }

// MARK: - Actions -

private func shareTipCard() {
let item = TipCodeShareItem.profile(
url: url,
displayName: displayName,
preview: previewCache.preview(for: sessionContainer.session.userID)
)
ShareSheet.present(activityItem: item) { _ in }
}

/// Shows the card; it runs its own scan-style reveal, and the toolbar glyph fades to Download alongside.
private func showProfileCard() {
withAnimation(ProfileCardScreen.fade) { router.isShowingProfileCard = true }
Expand All @@ -338,18 +337,12 @@ struct YouScreen: View {
private func handleShareChoice() {
defer { shareChoice = nil }
switch shareChoice {
case .share: shareTipCard()
case .share: isSharingToChats = true
case .showCard: showProfileCard()
case .copyLink: copyLink()
case nil: break
}
}

private func copyLink() {
UIPasteboard.general.string = url.absoluteString
toasts.show(.init("Copied", systemImage: "checkmark.circle.fill", duration: .seconds(2)))
}

/// Opens the claim screen once the balance clears the minimum, and the
/// balance gate until then.
private func claimUsername() {
Expand Down
30 changes: 3 additions & 27 deletions Flipcash/Core/Screens/Profile/ChatProfileScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ struct ChatProfileScreen: View {
@Environment(ToastController.self) private var toasts

@State private var isInviting = false
@State private var isShowingShare = false
@State private var shareChoice: ProfileShareChoice?
@State private var isPickingMuteDuration = false
@State private var isReporting = false
@State private var isShowingE2ee = false
Expand Down Expand Up @@ -171,16 +169,8 @@ struct ChatProfileScreen: View {
}
}
.dialog(item: $dialogItem)
.sheet(isPresented: $isShowingShare, onDismiss: handleShareChoice) {
ProfileShareSheet(
title: "Share Group",
subtitle: title,
shareRow: ("Share on Flipcash", Image(systemName: "paperplane")),
offersCard: false
) { shareChoice = $0 }
}
.sheet(isPresented: $isInviting) {
GroupInviteSheet(conversationID: conversationID, isPresented: $isInviting) { chatID in
.fullScreenCover(isPresented: $isInviting) {
ShareToChatsSheet(subject: .group(conversationID), isPresented: $isInviting) { chatID in
router.push(.tipConversation(chatID))
}
}
Expand Down Expand Up @@ -430,26 +420,12 @@ struct ChatProfileScreen: View {

private var shareButton: some View {
ProfileActionCircle(image: Image.asset(.shareOS)) {
isShowingShare = true
openInvite()
}
.accessibilityLabel("Share group")
.accessibilityIdentifier("chat-profile-share")
}

/// Runs once the share sheet is gone, so a following sheet doesn't present over it.
private func handleShareChoice() {
defer { shareChoice = nil }
switch shareChoice {
case .share:
openInvite()
case .copyLink:
UIPasteboard.general.string = URL.groupChatInvite(for: conversationID).absoluteString
toasts.show(.init("Copied", systemImage: "checkmark.circle.fill", duration: .seconds(2)))
case .showCard, nil:
break
}
}

private func openInvite() {
Analytics.groupInviteSheetOpened(
source: .profile,
Expand Down
Loading
Loading