111 lines
3.8 KiB
Swift
111 lines
3.8 KiB
Swift
|
|
import SwiftUI
|
||
|
|
|
||
|
|
/// The conversation screen for a revealed connection.
|
||
|
|
///
|
||
|
|
/// This is where two people who met in the real world actually talk. The UI
|
||
|
|
/// is deliberately calm and human — no engagement-baiting, no streaks, no
|
||
|
|
/// algorithms. Just a conversation between two people who chose to meet.
|
||
|
|
struct ChatView: View {
|
||
|
|
@EnvironmentObject private var vm: ChatViewModel
|
||
|
|
@EnvironmentObject private var revealFlow: RevealFlow
|
||
|
|
@State private var input = ""
|
||
|
|
|
||
|
|
let connection: Connection
|
||
|
|
|
||
|
|
var body: some View {
|
||
|
|
VStack(spacing: 0) {
|
||
|
|
// Header
|
||
|
|
HStack(spacing: 12) {
|
||
|
|
Circle()
|
||
|
|
.fill(Color.green.opacity(0.2))
|
||
|
|
.frame(width: 40, height: 40)
|
||
|
|
.overlay(Text(String(connection.displayName.prefix(1))))
|
||
|
|
|
||
|
|
VStack(alignment: .leading, spacing: 2) {
|
||
|
|
Text(connection.displayName)
|
||
|
|
.font(.headline)
|
||
|
|
Text("Met in person · \(connection.createdAt, format: .dateTime.month().day())")
|
||
|
|
.font(.caption)
|
||
|
|
.foregroundColor(.secondary)
|
||
|
|
}
|
||
|
|
Spacer()
|
||
|
|
Image(systemName: "lock.fill")
|
||
|
|
.font(.caption)
|
||
|
|
.foregroundColor(.green)
|
||
|
|
.accessibilityLabel("End-to-end encrypted")
|
||
|
|
}
|
||
|
|
.padding()
|
||
|
|
.background(Color(.secondarySystemBackground))
|
||
|
|
|
||
|
|
Divider()
|
||
|
|
|
||
|
|
// Messages
|
||
|
|
ScrollViewReader { proxy in
|
||
|
|
ScrollView {
|
||
|
|
LazyVStack(spacing: 12) {
|
||
|
|
ForEach(vm.messages) { message in
|
||
|
|
MessageBubble(message: message, isMine: message.senderID == "me")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.padding()
|
||
|
|
}
|
||
|
|
.onChange(of: vm.messages.count) { _, _ in
|
||
|
|
if let last = vm.messages.last {
|
||
|
|
withAnimation {
|
||
|
|
proxy.scrollTo(last.id, anchor: .bottom)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Input
|
||
|
|
HStack(spacing: 12) {
|
||
|
|
TextField("Say hello…", text: $input, axis: .vertical)
|
||
|
|
.lineLimit(1...4)
|
||
|
|
.padding(10)
|
||
|
|
.background(Color(.secondarySystemBackground))
|
||
|
|
.clipShape(RoundedRectangle(cornerRadius: 18))
|
||
|
|
|
||
|
|
Button {
|
||
|
|
vm.draft = input
|
||
|
|
input = ""
|
||
|
|
Task { await vm.send() }
|
||
|
|
} label: {
|
||
|
|
Image(systemName: "arrow.up.circle.fill")
|
||
|
|
.font(.system(size: 28))
|
||
|
|
}
|
||
|
|
.disabled(input.trimmingCharacters(in: .whitespaces).isEmpty)
|
||
|
|
}
|
||
|
|
.padding()
|
||
|
|
}
|
||
|
|
.navigationTitle("")
|
||
|
|
.navigationBarTitleDisplayMode(.inline)
|
||
|
|
.onAppear {
|
||
|
|
// Use the E2E session key established at reveal time.
|
||
|
|
vm.start(with: connection, sessionKey: revealFlow.currentSessionKey())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// A single message bubble.
|
||
|
|
struct MessageBubble: View {
|
||
|
|
@EnvironmentObject private var vm: ChatViewModel
|
||
|
|
let message: ChatMessage
|
||
|
|
let isMine: Bool
|
||
|
|
|
||
|
|
var body: some View {
|
||
|
|
HStack {
|
||
|
|
if isMine { Spacer() }
|
||
|
|
Text(vm.text(for: message))
|
||
|
|
.padding(.horizontal, 14)
|
||
|
|
.padding(.vertical, 10)
|
||
|
|
.background(
|
||
|
|
RoundedRectangle(cornerRadius: 18)
|
||
|
|
.fill(isMine ? Color.green : Color(.secondarySystemBackground))
|
||
|
|
)
|
||
|
|
.foregroundColor(isMine ? .white : .primary)
|
||
|
|
if !isMine { Spacer() }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|