mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 17:25:22 +00:00
Initial commit: Bluetooth mesh chat app with end-to-end encryption
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Combine
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
class ChatViewModel: ObservableObject {
|
||||
@Published var messages: [BitchatMessage] = []
|
||||
@Published var connectedPeers: [String] = []
|
||||
@Published var nickname: String = "" {
|
||||
didSet {
|
||||
nicknameSaveTimer?.invalidate()
|
||||
nicknameSaveTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
|
||||
self.saveNickname()
|
||||
}
|
||||
}
|
||||
}
|
||||
@Published var isConnected = false
|
||||
|
||||
let meshService = BluetoothMeshService()
|
||||
private let userDefaults = UserDefaults.standard
|
||||
private let nicknameKey = "bitchat.nickname"
|
||||
private var nicknameSaveTimer: Timer?
|
||||
|
||||
init() {
|
||||
loadNickname()
|
||||
meshService.delegate = self
|
||||
|
||||
// Start mesh service immediately
|
||||
meshService.startServices()
|
||||
}
|
||||
|
||||
private func loadNickname() {
|
||||
if let savedNickname = userDefaults.string(forKey: nicknameKey) {
|
||||
nickname = savedNickname
|
||||
} else {
|
||||
nickname = "user\(Int.random(in: 1000...9999))"
|
||||
saveNickname()
|
||||
}
|
||||
}
|
||||
|
||||
func saveNickname() {
|
||||
userDefaults.set(nickname, forKey: nicknameKey)
|
||||
userDefaults.synchronize() // Force immediate save
|
||||
}
|
||||
|
||||
func sendMessage(_ content: String) {
|
||||
guard !content.isEmpty else { return }
|
||||
|
||||
// Add message to local display
|
||||
let message = BitchatMessage(
|
||||
sender: nickname,
|
||||
content: content,
|
||||
timestamp: Date(),
|
||||
isRelay: false,
|
||||
originalSender: nil
|
||||
)
|
||||
messages.append(message)
|
||||
|
||||
// Send via mesh
|
||||
meshService.sendMessage(content)
|
||||
}
|
||||
|
||||
|
||||
func formatTimestamp(_ date: Date) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm:ss"
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
func formatMessage(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString {
|
||||
var result = AttributedString()
|
||||
|
||||
let isDark = colorScheme == .dark
|
||||
let primaryColor = isDark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||
let secondaryColor = primaryColor.opacity(0.7)
|
||||
|
||||
let timestamp = AttributedString("[\(formatTimestamp(message.timestamp))] ")
|
||||
var timestampStyle = AttributeContainer()
|
||||
timestampStyle.foregroundColor = secondaryColor
|
||||
timestampStyle.font = .system(size: 12, design: .monospaced)
|
||||
result.append(timestamp.mergingAttributes(timestampStyle))
|
||||
|
||||
if message.sender == "system" {
|
||||
let content = AttributedString("* \(message.content) *")
|
||||
var contentStyle = AttributeContainer()
|
||||
contentStyle.foregroundColor = secondaryColor
|
||||
contentStyle.font = .system(size: 12, design: .monospaced).italic()
|
||||
result.append(content.mergingAttributes(contentStyle))
|
||||
} else {
|
||||
let sender = AttributedString("<\(message.sender)> ")
|
||||
var senderStyle = AttributeContainer()
|
||||
senderStyle.foregroundColor = message.sender == nickname ? primaryColor : primaryColor.opacity(0.9)
|
||||
senderStyle.font = .system(size: 12, weight: .medium, design: .monospaced)
|
||||
result.append(sender.mergingAttributes(senderStyle))
|
||||
|
||||
let content = AttributedString(message.content)
|
||||
var contentStyle = AttributeContainer()
|
||||
contentStyle.font = .system(size: 14, design: .monospaced)
|
||||
contentStyle.foregroundColor = isDark ? Color.white : Color.black
|
||||
result.append(content.mergingAttributes(contentStyle))
|
||||
|
||||
if message.isRelay, let originalSender = message.originalSender {
|
||||
let relay = AttributedString(" (via \(originalSender))")
|
||||
var relayStyle = AttributeContainer()
|
||||
relayStyle.foregroundColor = secondaryColor
|
||||
relayStyle.font = .system(size: 11, design: .monospaced)
|
||||
result.append(relay.mergingAttributes(relayStyle))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
extension ChatViewModel: BitchatDelegate {
|
||||
func didReceiveMessage(_ message: BitchatMessage) {
|
||||
messages.append(message)
|
||||
|
||||
#if os(iOS)
|
||||
// Haptic feedback for new messages
|
||||
let impactFeedback = UIImpactFeedbackGenerator(style: .light)
|
||||
impactFeedback.impactOccurred()
|
||||
#endif
|
||||
}
|
||||
|
||||
func didConnectToPeer(_ peerID: String) {
|
||||
isConnected = true
|
||||
let systemMessage = BitchatMessage(
|
||||
sender: "system",
|
||||
content: "\(peerID) has joined",
|
||||
timestamp: Date(),
|
||||
isRelay: false,
|
||||
originalSender: nil
|
||||
)
|
||||
messages.append(systemMessage)
|
||||
}
|
||||
|
||||
func didDisconnectFromPeer(_ peerID: String) {
|
||||
let systemMessage = BitchatMessage(
|
||||
sender: "system",
|
||||
content: "\(peerID) has left",
|
||||
timestamp: Date(),
|
||||
isRelay: false,
|
||||
originalSender: nil
|
||||
)
|
||||
messages.append(systemMessage)
|
||||
}
|
||||
|
||||
func didUpdatePeerList(_ peers: [String]) {
|
||||
connectedPeers = peers
|
||||
isConnected = !peers.isEmpty
|
||||
|
||||
// If we just disconnected from all peers, ensure UI updates
|
||||
if peers.isEmpty && isConnected {
|
||||
isConnected = false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user