mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 01:45:20 +00:00
* Centralize UI theme colors into semantic ThemePalette tokens Introduce AppTheme/ThemePalette (Utils/Theme.swift) with an environment key and @ThemedPalette property wrapper, persisted via @AppStorage. Views now resolve background/primary/secondary/accentBlue/alertRed/ divider tokens from the environment instead of computing colors inline, removing the backgroundColor/textColor/secondaryTextColor prop-drilling through the header, composer, and people-sheet hierarchy. Matrix theme output is pixel-identical; this is groundwork for a user-selectable theme switcher. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add liquid glass theme with in-app appearance switcher Add a .liquidGlass AppTheme alongside the matrix terminal theme, selectable from a one-line APPEARANCE row in the app info sheet (persisted via @AppStorage, applies live). Liquid glass renders system fonts and colors over a subtle gradient backdrop, with the header and composer floating as Liquid Glass panels (real .glassEffect() on iOS/macOS 26, compiler-gated with an ultraThinMaterial fallback for older SDKs). The message list scrolls underneath the chrome via safe-area insets, and all sheets share the same backdrop and surface language. The matrix theme is unchanged. Details: - Theme is threaded through ChatMessageFormatter so message AttributedStrings switch font design per theme; the per-message format cache gains a variant key so themes never serve each other's cached strings - New palette tokens: accent (interactive tint) and locationAccent (geohash green), replacing scattered hardcoded greens/blues in the voice note, waveform, verification, and people-sheet views - Header controls get full-height tap targets and the people count becomes a real Button (previously a tap gesture on the cluster) - CommandSuggestionsView renders nothing when empty instead of a zero-height view that pushed the composer input off-center - New appearance strings localized for all 29 catalog languages Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
127 lines
4.5 KiB
Swift
127 lines
4.5 KiB
Swift
//
|
|
// BitchatApp.swift
|
|
// bitchat
|
|
//
|
|
// This is free and unencumbered software released into the public domain.
|
|
// For more information, see <https://unlicense.org>
|
|
//
|
|
|
|
import SwiftUI
|
|
import UserNotifications
|
|
|
|
@main
|
|
struct BitchatApp: App {
|
|
static let bundleID = Bundle.main.bundleIdentifier ?? "chat.bitchat"
|
|
static let groupID = "group.\(bundleID)"
|
|
|
|
@StateObject private var runtime: AppRuntime
|
|
@AppStorage(AppTheme.storageKey) private var appThemeRawValue = AppTheme.matrix.rawValue
|
|
#if os(iOS)
|
|
@Environment(\.scenePhase) var scenePhase
|
|
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
|
#elseif os(macOS)
|
|
@NSApplicationDelegateAdaptor(MacAppDelegate.self) var appDelegate
|
|
#endif
|
|
|
|
init() {
|
|
_runtime = StateObject(wrappedValue: AppRuntime())
|
|
UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
|
|
}
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
ContentView()
|
|
.environment(\.appTheme, AppTheme(rawValue: appThemeRawValue) ?? .matrix)
|
|
.environmentObject(runtime.publicChatModel)
|
|
.environmentObject(runtime.privateInboxModel)
|
|
.environmentObject(runtime.privateConversationModel)
|
|
.environmentObject(runtime.verificationModel)
|
|
.environmentObject(runtime.conversationUIModel)
|
|
.environmentObject(runtime.locationChannelsModel)
|
|
.environmentObject(runtime.peerListModel)
|
|
.environmentObject(runtime.appChromeModel)
|
|
.onAppear {
|
|
appDelegate.runtime = runtime
|
|
runtime.start()
|
|
}
|
|
.onOpenURL { url in
|
|
runtime.handleOpenURL(url)
|
|
}
|
|
#if os(iOS)
|
|
.onChange(of: scenePhase) { newPhase in
|
|
runtime.handleScenePhaseChange(newPhase)
|
|
}
|
|
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
|
|
runtime.handleDidBecomeActiveNotification()
|
|
}
|
|
#elseif os(macOS)
|
|
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
|
runtime.handleMacDidBecomeActiveNotification()
|
|
}
|
|
#endif
|
|
}
|
|
#if os(macOS)
|
|
.windowStyle(.hiddenTitleBar)
|
|
.windowResizability(.contentSize)
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#if os(iOS)
|
|
final class AppDelegate: NSObject, UIApplicationDelegate {
|
|
weak var runtime: AppRuntime?
|
|
|
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
|
true
|
|
}
|
|
|
|
func applicationWillTerminate(_ application: UIApplication) {
|
|
runtime?.applicationWillTerminate()
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if os(macOS)
|
|
import AppKit
|
|
|
|
final class MacAppDelegate: NSObject, NSApplicationDelegate {
|
|
weak var runtime: AppRuntime?
|
|
|
|
func applicationWillTerminate(_ notification: Notification) {
|
|
runtime?.applicationWillTerminate()
|
|
}
|
|
|
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
|
true
|
|
}
|
|
}
|
|
#endif
|
|
|
|
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
|
|
static let shared = NotificationDelegate()
|
|
weak var runtime: AppRuntime?
|
|
|
|
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
|
let identifier = response.notification.request.identifier
|
|
let userInfo = response.notification.request.content.userInfo
|
|
|
|
Task { @MainActor in
|
|
self.runtime?.handleNotificationResponse(identifier: identifier, userInfo: userInfo)
|
|
}
|
|
completionHandler()
|
|
}
|
|
|
|
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
|
let identifier = notification.request.identifier
|
|
let userInfo = notification.request.content.userInfo
|
|
|
|
Task {
|
|
let options = await self.runtime?.presentationOptions(
|
|
forNotificationIdentifier: identifier,
|
|
userInfo: userInfo
|
|
) ?? [.banner, .sound]
|
|
completionHandler(options)
|
|
}
|
|
}
|
|
}
|