montana/macOS/MontanaPresence/CallsView.swift

359 lines
12 KiB
Swift
Raw Normal View History

import SwiftUI
/// Montana Calls Audio & Video
/// Управление звонками (1 Ɉ/сек для владельцев номеров)
struct CallsView: View {
@EnvironmentObject var engine: PresenceEngine
@State private var recipientInput = ""
@State private var callType: CallType = .audio
@State private var callHistory: [CallRecord] = []
@State private var showSidebar = false
@State private var audioPricing = 1
@State private var videoPricing = 1
@State private var showCallAlert = false
private let gold = Color(red: 0.85, green: 0.68, blue: 0.25)
enum CallType: String, CaseIterable {
case audio = "Аудио"
case video = "Видео"
}
var body: some View {
ZStack(alignment: .leading) {
VStack(spacing: 0) {
// BURGER MENU BUTTON
HStack {
Button(action: { withAnimation(.easeInOut(duration: 0.3)) { showSidebar = true } }) {
Image(systemName: "line.3.horizontal")
.font(.system(size: 18, weight: .medium))
.foregroundColor(gold)
.padding(8)
}
.buttonStyle(.plain)
Spacer()
}
.padding(.horizontal, 12)
.padding(.top, 8)
// Header
header
Divider()
// Main content
ScrollView {
VStack(spacing: 24) {
// Call interface
callCard
// Pricing info
pricingCard
// Call history
if !callHistory.isEmpty {
historySection
}
}
.padding()
}
}
.background(Color(NSColor.windowBackgroundColor))
.onAppear {
loadCallHistory()
loadPricing()
}
.alert("Звонки Montana", isPresented: $showCallAlert) {
Button("OK") { }
} message: {
Text("VoIP звонки скоро будут доступны. Следите за обновлениями!")
}
// Sidebar overlay
if showSidebar {
Color.black.opacity(0.3)
.ignoresSafeArea()
.onTapGesture {
withAnimation(.easeInOut(duration: 0.3)) {
showSidebar = false
}
}
SharedSidebar(isVisible: $showSidebar)
.transition(.move(edge: .leading))
}
}
}
// MARK: - Header
private var header: some View {
HStack(spacing: 12) {
// Call icon
Circle()
.fill(
LinearGradient(
colors: [
Color(red: 0.0, green: 0.83, blue: 1.0), // #00d4ff cyan
Color(red: 0.48, green: 0.18, blue: 1.0) // #7b2fff purple
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 40, height: 40)
.overlay(
Image(systemName: "phone.fill")
.font(.system(size: 18))
.foregroundColor(.white)
)
VStack(alignment: .leading, spacing: 2) {
Text("Звонки Montana")
.font(.headline)
Text("Аудио и видео звонки")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
}
.padding()
}
// MARK: - Call Card
private var callCard: some View {
VStack(spacing: 16) {
Text("Совершить звонок")
.font(.title3)
.fontWeight(.semibold)
// Recipient input
HStack(spacing: 8) {
TextField("alice@efir.org", text: $recipientInput)
.textFieldStyle(.plain)
.font(.system(size: 16))
.padding(10)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
Text("или")
.foregroundColor(.secondary)
.font(.caption)
TextField("+montana-000042", text: $recipientInput)
.textFieldStyle(.plain)
.font(.system(size: 16, design: .monospaced))
.padding(10)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
}
// Call type picker
Picker("Тип звонка", selection: $callType) {
ForEach(CallType.allCases, id: \.self) { type in
HStack {
Image(systemName: type == .audio ? "mic.fill" : "video.fill")
Text(type.rawValue)
}
.tag(type)
}
}
.pickerStyle(.segmented)
// Call button
Button(action: initiateCall) {
HStack(spacing: 8) {
Image(systemName: callType == .audio ? "phone.fill" : "video.fill")
Text("Позвонить")
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(recipientInput.isEmpty ? Color.gray : Color.green)
.foregroundColor(.white)
.cornerRadius(8)
}
.buttonStyle(.plain)
.disabled(recipientInput.isEmpty)
}
.padding()
.background(Color.blue.opacity(0.05))
.cornerRadius(12)
}
// MARK: - Pricing Card
private var pricingCard: some View {
VStack(spacing: 16) {
Text("💰 Стоимость звонков")
.font(.title3)
.fontWeight(.semibold)
VStack(spacing: 12) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("Аудио звонки")
.font(.headline)
Text("Голосовая связь")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
HStack(spacing: 4) {
Text("1")
.font(.title2)
.fontWeight(.bold)
Text("Ɉ/сек")
.font(.title3)
}
.foregroundColor(Color(red: 0.0, green: 0.83, blue: 1.0))
}
.padding()
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("Видео звонки")
.font(.headline)
Text("Видеосвязь HD")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
HStack(spacing: 4) {
Text("1")
.font(.title2)
.fontWeight(.bold)
Text("Ɉ/сек")
.font(.title3)
}
.foregroundColor(Color(red: 0.0, green: 0.83, blue: 1.0))
}
.padding()
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
}
Text("⚠️ Для звонков нужен Montana номер")
.font(.caption)
.foregroundColor(.orange)
}
.padding()
.background(Color.green.opacity(0.05))
.cornerRadius(12)
}
// MARK: - History Section
private var historySection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("История звонков")
.font(.title3)
.fontWeight(.semibold)
ForEach(callHistory) { record in
HStack {
Image(systemName: record.type == .audio ? "phone.fill" : "video.fill")
.foregroundColor(Color(red: 0.0, green: 0.83, blue: 1.0))
VStack(alignment: .leading, spacing: 2) {
Text(record.recipient)
.font(.system(.body, design: .monospaced))
Text(record.timestamp, style: .relative)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
VStack(alignment: .trailing, spacing: 2) {
Text(formatDuration(record.duration))
.font(.caption)
Text("\(record.cost) Ɉ")
.font(.caption)
.fontWeight(.semibold)
}
}
.padding()
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
}
}
}
// MARK: - Actions
private func loadCallHistory() {
guard let addr = engine.address else { return }
Task { @MainActor in
do {
let events = try await engine.api.fetchMyEvents(address: addr)
callHistory = events.compactMap { event in
guard let type = event["event_type"] as? String,
(type == "call_audio" || type == "call_video"),
let metadata = event["metadata"] as? [String: Any],
let duration = metadata["duration_seconds"] as? Int,
let amount = event["amount"] as? Int,
let toAddr = event["to_address"] as? String else { return nil }
let callType: CallType = type == "call_video" ? .video : .audio
let ts = event["timestamp"] as? String ?? ""
let df = ISO8601DateFormatter()
let date = df.date(from: ts) ?? Date()
return CallRecord(recipient: toAddr, type: callType, duration: duration, cost: amount, timestamp: date)
}
} catch {
callHistory = []
}
}
}
private func loadPricing() {
Task { @MainActor in
do {
let (audio, video) = try await engine.api.fetchCallPricing()
audioPricing = audio
videoPricing = video
} catch {
audioPricing = 1
videoPricing = 1
}
}
}
private func initiateCall() {
showCallAlert = true
}
private func formatDuration(_ seconds: Int) -> String {
let minutes = seconds / 60
let secs = seconds % 60
return String(format: "%d:%02d", minutes, secs)
}
}
// MARK: - Models
struct CallRecord: Identifiable {
let id = UUID()
let recipient: String
let type: CallsView.CallType
let duration: Int // seconds
let cost: Int // Ɉ
let timestamp: Date
}
// MARK: - Preview
#Preview {
CallsView()
.environmentObject(PresenceEngine.shared)
.frame(width: 600, height: 500)
}