forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathConversationShareLink.swift
More file actions
162 lines (148 loc) · 5.11 KB
/
Copy pathConversationShareLink.swift
File metadata and controls
162 lines (148 loc) · 5.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import AppKit
import OmiTheme
import SwiftUI
/// Transient user-facing feedback for the "copy share link" affordance.
///
/// Minting a conversation share link is not a plain read: the backend flips the
/// conversation's visibility to "shared" before the `https://h.omi.me` URL
/// resolves (`APIClient.getConversationShareLink`). The copied confirmation
/// therefore has to disclose who can open the link. The wording lives here so
/// every surface (conversation detail, meeting-notes chat card) discloses the
/// same thing.
enum ConversationShareLinkFeedback: Equatable {
case copied
case failed
var message: String {
switch self {
case .copied: return "Link copied — anyone with the link can view"
case .failed: return "Couldn't create link"
}
}
var systemImage: String {
switch self {
case .copied: return "checkmark"
case .failed: return "exclamationmark.triangle"
}
}
/// How long the transient confirmation stays visible.
static let displaySeconds: TimeInterval = 4
}
enum ConversationShareLinkAction {
/// Mints a share link and copies it to the pasteboard.
///
/// The URL is copied only when minting succeeded: the mint call is the
/// visibility mutation that makes the URL resolve, so copying after a
/// failure would hand the user a link that may 404 while the UI reads as
/// success. The pasteboard write's own result is honored too — "Link
/// copied" is never shown for a write NSPasteboard rejected. Failure
/// feedback is non-blocking; callers keep every other affordance (like
/// opening the conversation) available.
@MainActor
static func run(
mintLink: () async throws -> String,
copyToPasteboard: (String) -> Bool,
onFailure: (Error) -> Void = { _ in }
) async -> ConversationShareLinkFeedback {
do {
let link = try await mintLink()
return copyToPasteboard(link) ? .copied : .failed
} catch {
onFailure(error)
return .failed
}
}
}
/// The 28pt header-toolbar control for copying a conversation share link.
///
/// Minting the link sets the conversation's visibility to shared, so the
/// transient confirmation says who can open it instead of reading as a plain
/// copy. The message renders as an overlay anchored under the glyph so the
/// header row never reflows and neighboring titles are never crushed while
/// feedback shows.
struct ConversationShareLinkButton: View {
let conversationId: String
var canShare: Bool = true
var onCopied: (() -> Void)? = nil
@State private var isCopyingLink = false
@State private var feedback: ConversationShareLinkFeedback?
@State private var feedbackGeneration = 0
var body: some View {
Button(action: { Task { await copyLink() } }) {
Image(systemName: icon)
.scaledFont(size: OmiType.body)
.foregroundColor(color)
.frame(width: 28, height: 28)
.background(
Circle()
.fill(Ink.rowFillHover)
)
}
.buttonStyle(.plain)
.disabled(isCopyingLink || !canShare)
.help(
canShare
? "Copy share link — anyone with the link can view"
: "Sharing is unavailable while the transcript is locked"
)
.accessibilityLabel(feedback?.message ?? "Copy share link")
.overlay(alignment: .trailing) {
if let feedback {
Text(feedback.message)
.scaledFont(size: OmiType.caption, weight: .medium)
.foregroundColor(color)
.padding(.horizontal, OmiSpacing.md)
.padding(.vertical, OmiSpacing.xs)
.background(
Capsule()
.fill(Ink.surface)
)
.overlay(
Capsule()
.stroke(Ink.glassEdge, lineWidth: 1)
)
.fixedSize()
.offset(y: 34)
.allowsHitTesting(false)
.zIndex(1)
}
}
}
private var icon: String {
if isCopyingLink { return "arrow.triangle.2.circlepath" }
return feedback?.systemImage ?? "link"
}
private var color: Color {
switch feedback {
case .copied: return Ink.listeningGreen
case .failed: return Ink.errorRed
case nil: return Ink.secondary
}
}
private func copyLink() async {
guard !isCopyingLink, canShare else { return }
isCopyingLink = true
defer { isCopyingLink = false }
let outcome = await ConversationShareLinkAction.run(
mintLink: { try await APIClient.shared.getConversationShareLink(id: conversationId) },
copyToPasteboard: { link in
NSPasteboard.general.clearContents()
return NSPasteboard.general.setString(link, forType: .string)
},
onFailure: { logError("Failed to get share link", error: $0) }
)
if outcome == .copied {
onCopied?()
}
show(outcome)
}
private func show(_ outcome: ConversationShareLinkFeedback) {
feedback = outcome
feedbackGeneration += 1
let generation = feedbackGeneration
DispatchQueue.main.asyncAfter(deadline: .now() + ConversationShareLinkFeedback.displaySeconds) {
// A newer click owns the confirmation; only the latest one clears it.
guard feedbackGeneration == generation else { return }
feedback = nil
}
}
}