initial commit: Proximity iOS app proposal, full Swift code scaffold, reference backend, testing docs, generated Xcode project
This commit is contained in:
110
Proximity/Views/ChatView.swift
Normal file
110
Proximity/Views/ChatView.swift
Normal file
@@ -0,0 +1,110 @@
|
||||
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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user