Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bc4a9a19b | |||
| e34faf56c7 | |||
| 5dad4e22f5 | |||
| f1290d7e38 |
@@ -9,7 +9,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SimpleXChat
|
||||
import SwiftUI
|
||||
|
||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
@@ -18,7 +17,6 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
if #available(iOS 17.0, *) { trackKeyboard() }
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(pasteboardChanged), name: UIPasteboard.changedNotification, object: nil)
|
||||
removePasscodesIfReinstalled()
|
||||
prepareForLaunch()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -143,10 +141,6 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private func prepareForLaunch() {
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
static func keepScreenOn(_ on: Bool) {
|
||||
UIApplication.shared.isIdleTimerDisabled = on
|
||||
}
|
||||
@@ -154,79 +148,13 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
|
||||
class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
|
||||
var window: UIWindow?
|
||||
static var windowStatic: UIWindow?
|
||||
var windowScene: UIWindowScene?
|
||||
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
UITableView.appearance().backgroundColor = .clear
|
||||
guard let windowScene = scene as? UIWindowScene else { return }
|
||||
self.windowScene = windowScene
|
||||
window = windowScene.keyWindow
|
||||
SceneDelegate.windowStatic = windowScene.keyWindow
|
||||
migrateAccentColorAndTheme()
|
||||
ThemeManager.applyTheme(currentThemeDefault.get())
|
||||
ThemeManager.adjustWindowStyle()
|
||||
}
|
||||
|
||||
private func migrateAccentColorAndTheme() {
|
||||
let defs = UserDefaults.standard
|
||||
/// For checking migration
|
||||
// themeOverridesDefault.set([])
|
||||
// currentThemeDefault.set(DefaultTheme.SYSTEM_THEME_NAME)
|
||||
// defs.set(0.5, forKey: DEFAULT_ACCENT_COLOR_RED)
|
||||
// defs.set(0.3, forKey: DEFAULT_ACCENT_COLOR_GREEN)
|
||||
// defs.set(0.8, forKey: DEFAULT_ACCENT_COLOR_BLUE)
|
||||
|
||||
let userInterfaceStyle = getUserInterfaceStyleDefault()
|
||||
if defs.double(forKey: DEFAULT_ACCENT_COLOR_GREEN) == 0 && userInterfaceStyle == .unspecified {
|
||||
// No migration needed or already migrated
|
||||
return
|
||||
}
|
||||
|
||||
let defaultAccentColor = Color(cgColor: CGColor(red: 0.000, green: 0.533, blue: 1.000, alpha: 1))
|
||||
let accentColor = Color(cgColor: getUIAccentColorDefault())
|
||||
if accentColor != defaultAccentColor {
|
||||
let colors = ThemeColors(primary: accentColor.toReadableHex())
|
||||
var overrides = themeOverridesDefault.get()
|
||||
var themeIds = currentThemeIdsDefault.get()
|
||||
switch userInterfaceStyle {
|
||||
case .light:
|
||||
let light = ThemeOverrides(base: DefaultTheme.LIGHT, colors: colors, wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename))
|
||||
overrides.append(light)
|
||||
themeOverridesDefault.set(overrides)
|
||||
themeIds[DefaultTheme.LIGHT.themeName] = light.themeId
|
||||
currentThemeIdsDefault.set(themeIds)
|
||||
ThemeManager.applyTheme(DefaultTheme.LIGHT.themeName)
|
||||
case .dark:
|
||||
let dark = ThemeOverrides(base: DefaultTheme.DARK, colors: colors, wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename))
|
||||
overrides.append(dark)
|
||||
themeOverridesDefault.set(overrides)
|
||||
themeIds[DefaultTheme.DARK.themeName] = dark.themeId
|
||||
currentThemeIdsDefault.set(themeIds)
|
||||
ThemeManager.applyTheme(DefaultTheme.DARK.themeName)
|
||||
case .unspecified:
|
||||
let light = ThemeOverrides(base: DefaultTheme.LIGHT, colors: colors, wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename))
|
||||
let dark = ThemeOverrides(base: DefaultTheme.DARK, colors: colors, wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename))
|
||||
overrides.append(light)
|
||||
overrides.append(dark)
|
||||
themeOverridesDefault.set(overrides)
|
||||
themeIds[DefaultTheme.LIGHT.themeName] = light.themeId
|
||||
themeIds[DefaultTheme.DARK.themeName] = dark.themeId
|
||||
currentThemeIdsDefault.set(themeIds)
|
||||
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
|
||||
@unknown default: ()
|
||||
}
|
||||
} else if userInterfaceStyle != .unspecified {
|
||||
let themeName = switch userInterfaceStyle {
|
||||
case .light: DefaultTheme.LIGHT.themeName
|
||||
case .dark: DefaultTheme.DARK.themeName
|
||||
default: DefaultTheme.SYSTEM_THEME_NAME
|
||||
}
|
||||
ThemeManager.applyTheme(themeName)
|
||||
}
|
||||
defs.removeObject(forKey: DEFAULT_ACCENT_COLOR_RED)
|
||||
defs.removeObject(forKey: DEFAULT_ACCENT_COLOR_GREEN)
|
||||
defs.removeObject(forKey: DEFAULT_ACCENT_COLOR_BLUE)
|
||||
defs.removeObject(forKey: DEFAULT_USER_INTERFACE_STYLE)
|
||||
window?.tintColor = UIColor(cgColor: getUIAccentColorDefault())
|
||||
window?.overrideUserInterfaceStyle = getUserInterfaceStyleDefault()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "wallpaper_cats@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_cats@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_cats@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 50 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "wallpaper_flowers@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_flowers@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_flowers@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 131 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "wallpaper_hearts@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_hearts@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_hearts@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 79 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "wallpaper_kids@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_kids@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_kids@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 168 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "wallpaper_school@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_school@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_school@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 233 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "wallpaper_travel@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_travel@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_travel@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 79 KiB |
@@ -14,8 +14,6 @@ struct ContentView: View {
|
||||
@ObservedObject var alertManager = AlertManager.shared
|
||||
@ObservedObject var callController = CallController.shared
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@EnvironmentObject var sceneDelegate: SceneDelegate
|
||||
|
||||
var contentAccessAuthenticationExtended: Bool
|
||||
|
||||
@@ -53,16 +51,6 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
allViews()
|
||||
.scrollContentBackground(.hidden)
|
||||
} else {
|
||||
// on iOS 15 scroll view background disabled in SceneDelegate
|
||||
allViews()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func allViews() -> some View {
|
||||
ZStack {
|
||||
let showCallArea = chatModel.activeCall != nil && chatModel.activeCall?.callState != .waitCapabilities && chatModel.activeCall?.callState != .invitationAccepted
|
||||
// contentView() has to be in a single branch, so that enabling authentication doesn't trigger re-rendering and close settings.
|
||||
@@ -150,17 +138,6 @@ struct ContentView: View {
|
||||
break
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
reactOnDarkThemeChanges()
|
||||
}
|
||||
.onChange(of: colorScheme) { scheme in
|
||||
// It's needed to update UI colors when iOS wants to make screenshot after going to background,
|
||||
// so when a user changes his global theme from dark to light or back, the app will adapt to it
|
||||
reactOnDarkThemeChanges()
|
||||
}
|
||||
.onChange(of: theme.name) { _ in
|
||||
ThemeManager.adjustWindowStyle()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func contentView() -> some View {
|
||||
@@ -247,8 +224,8 @@ struct ContentView: View {
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity )
|
||||
.background(
|
||||
Rectangle()
|
||||
.fill(theme.colors.background)
|
||||
)
|
||||
.fill(.background)
|
||||
)
|
||||
}
|
||||
|
||||
private func mainView() -> some View {
|
||||
|
||||
@@ -47,11 +47,7 @@ final class ChatModel: ObservableObject {
|
||||
@Published var onboardingStage: OnboardingStage?
|
||||
@Published var setDeliveryReceipts = false
|
||||
@Published var v3DBMigration: V3DBMigrationState = v3DBMigrationDefault.get()
|
||||
@Published var currentUser: User? {
|
||||
didSet {
|
||||
ThemeManager.applyTheme(currentThemeDefault.get())
|
||||
}
|
||||
}
|
||||
@Published var currentUser: User?
|
||||
@Published var users: [UserInfo] = []
|
||||
@Published var chatInitialized = false
|
||||
@Published var chatRunning: Bool?
|
||||
@@ -426,16 +422,6 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func updateCurrentUserUiThemes(uiThemes: ThemeModeOverrides?) {
|
||||
guard var current = currentUser else { return }
|
||||
current.uiThemes = uiThemes
|
||||
let i = users.firstIndex(where: { $0.user.userId == current.userId })
|
||||
if let i {
|
||||
users[i].user = current
|
||||
}
|
||||
currentUser = current
|
||||
}
|
||||
|
||||
func addLiveDummy(_ chatInfo: ChatInfo) -> ChatItem {
|
||||
let cItem = ChatItem.liveDummy(chatInfo.chatType)
|
||||
withAnimation {
|
||||
@@ -702,7 +688,7 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
return UnreadChatItemCounts(isNearBottom: totalBelow < 16, unreadBelow: unreadBelow)
|
||||
return UnreadChatItemCounts(totalBelow: totalBelow, unreadBelow: unreadBelow)
|
||||
}
|
||||
|
||||
func topItemInView(itemsInView: Set<String>) -> ChatItem? {
|
||||
@@ -740,7 +726,7 @@ struct NTFContactRequest {
|
||||
}
|
||||
|
||||
struct UnreadChatItemCounts: Equatable {
|
||||
var isNearBottom: Bool
|
||||
var totalBelow: Int
|
||||
var unreadBelow: Int
|
||||
}
|
||||
|
||||
|
||||
@@ -205,43 +205,6 @@ func moveTempFileFromURL(_ url: URL) -> CryptoFile? {
|
||||
}
|
||||
}
|
||||
|
||||
func saveWallpaperFile(url: URL) -> String? {
|
||||
let destFile = URL(fileURLWithPath: generateNewFileName(getWallpaperDirectory().path + "/" + "wallpaper", "jpg", fullPath: true))
|
||||
do {
|
||||
try FileManager.default.copyItem(atPath: url.path, toPath: destFile.path)
|
||||
return destFile.lastPathComponent
|
||||
} catch {
|
||||
logger.error("FileUtils.saveWallpaperFile error: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func saveWallpaperFile(image: UIImage) -> String? {
|
||||
let hasAlpha = imageHasAlpha(image)
|
||||
let destFile = URL(fileURLWithPath: generateNewFileName(getWallpaperDirectory().path + "/" + "wallpaper", hasAlpha ? "png" : "jpg", fullPath: true))
|
||||
let dataResized = resizeImageToDataSize(image, maxDataSize: 5_000_000, hasAlpha: hasAlpha)
|
||||
do {
|
||||
try dataResized!.write(to: destFile)
|
||||
return destFile.lastPathComponent
|
||||
} catch {
|
||||
logger.error("FileUtils.saveWallpaperFile error: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeWallpaperFile(fileName: String? = nil) {
|
||||
do {
|
||||
try FileManager.default.contentsOfDirectory(atPath: getWallpaperDirectory().path).forEach {
|
||||
if URL(fileURLWithPath: $0).lastPathComponent == fileName { try FileManager.default.removeItem(atPath: $0) }
|
||||
}
|
||||
} catch {
|
||||
logger.error("FileUtils.removeWallpaperFile error: \(error.localizedDescription)")
|
||||
}
|
||||
if let fileName {
|
||||
WallpaperType.cachedImages.removeValue(forKey: fileName)
|
||||
}
|
||||
}
|
||||
|
||||
func generateNewFileName(_ prefix: String, _ ext: String, fullPath: Bool = false) -> String {
|
||||
uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)", fullPath: fullPath)
|
||||
}
|
||||
|
||||
@@ -246,8 +246,14 @@ func apiSuspendChat(timeoutMicroseconds: Int) {
|
||||
logger.error("apiSuspendChat error: \(String(describing: r))")
|
||||
}
|
||||
|
||||
func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String, ctrl: chat_ctrl? = nil) throws {
|
||||
let r = chatSendCmdSync(.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder), ctrl)
|
||||
func apiSetTempFolder(tempFolder: String, ctrl: chat_ctrl? = nil) throws {
|
||||
let r = chatSendCmdSync(.setTempFolder(tempFolder: tempFolder), ctrl)
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetFilesFolder(filesFolder: String, ctrl: chat_ctrl? = nil) throws {
|
||||
let r = chatSendCmdSync(.setFilesFolder(filesFolder: filesFolder), ctrl)
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
@@ -836,21 +842,6 @@ func apiSetConnectionAlias(connId: Int64, localAlias: String) async throws -> Pe
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetUserUIThemes(userId: Int64, themes: ThemeModeOverrides?) async -> Bool {
|
||||
let r = await chatSendCmd(.apiSetUserUIThemes(userId: userId, themes: themes))
|
||||
if case .cmdOk = r { return true }
|
||||
logger.error("apiSetUserUIThemes bad response: \(String(describing: r))")
|
||||
return false
|
||||
}
|
||||
|
||||
func apiSetChatUIThemes(chatId: ChatId, themes: ThemeModeOverrides?) async -> Bool {
|
||||
let r = await chatSendCmd(.apiSetChatUIThemes(chatId: chatId, themes: themes))
|
||||
if case .cmdOk = r { return true }
|
||||
logger.error("apiSetChatUIThemes bad response: \(String(describing: r))")
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func apiCreateUserAddress() async throws -> String {
|
||||
let userId = try currentUserId("apiCreateUserAddress")
|
||||
let r = await chatSendCmd(.apiCreateMyAddress(userId: userId))
|
||||
@@ -1385,7 +1376,8 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni
|
||||
if encryptionStartedDefault.get() {
|
||||
encryptionStartedDefault.set(false)
|
||||
}
|
||||
try apiSetAppFilePaths(filesFolder: getAppFilesDirectory().path, tempFolder: getTempFilesDirectory().path, assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path)
|
||||
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
|
||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
||||
m.chatInitialized = true
|
||||
m.currentUser = try apiGetActiveUser()
|
||||
@@ -1470,7 +1462,8 @@ func startChatWithTemporaryDatabase(ctrl: chat_ctrl) throws -> User? {
|
||||
logger.debug("startChatWithTemporaryDatabase")
|
||||
let migrationActiveUser = try? apiGetActiveUser(ctrl: ctrl) ?? apiCreateActiveUser(Profile(displayName: "Temp", fullName: ""), ctrl: ctrl)
|
||||
try setNetworkConfig(getNetCfg(), ctrl: ctrl)
|
||||
try apiSetAppFilePaths(filesFolder: getMigrationTempFilesDirectory().path, tempFolder: getMigrationTempFilesDirectory().path, assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path, ctrl: ctrl)
|
||||
try apiSetTempFolder(tempFolder: getMigrationTempFilesDirectory().path, ctrl: ctrl)
|
||||
try apiSetFilesFolder(filesFolder: getMigrationTempFilesDirectory().path, ctrl: ctrl)
|
||||
_ = try apiStartChat(ctrl: ctrl)
|
||||
return migrationActiveUser
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ struct SimpleXApp: App {
|
||||
// so that it's computed by the time view renders, and not on event after rendering
|
||||
ContentView(contentAccessAuthenticationExtended: !authenticationExpired())
|
||||
.environmentObject(chatModel)
|
||||
.environmentObject(AppTheme.shared)
|
||||
.onOpenURL { url in
|
||||
logger.debug("ContentView.onOpenURL: \(url)")
|
||||
chatModel.appOpenUrl = url
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
//
|
||||
// Theme.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Avently on 14.06.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
var CurrentColors: ThemeManager.ActiveTheme = ThemeManager.currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
|
||||
var MenuTextColor: Color { if isInDarkTheme() { AppTheme.shared.colors.onBackground.opacity(0.8) } else { Color.black } }
|
||||
var NoteFolderIconColor: Color { AppTheme.shared.appColors.primaryVariant2 }
|
||||
|
||||
func isInDarkTheme() -> Bool { !CurrentColors.colors.isLight }
|
||||
|
||||
class AppTheme: ObservableObject, Equatable {
|
||||
static let shared = AppTheme(name: CurrentColors.name, base: CurrentColors.base, colors: CurrentColors.colors, appColors: CurrentColors.appColors, wallpaper: CurrentColors.wallpaper)
|
||||
|
||||
var name: String
|
||||
var base: DefaultTheme
|
||||
@ObservedObject var colors: Colors
|
||||
@ObservedObject var appColors: AppColors
|
||||
@ObservedObject var wallpaper: AppWallpaper
|
||||
|
||||
init(name: String, base: DefaultTheme, colors: Colors, appColors: AppColors, wallpaper: AppWallpaper) {
|
||||
self.name = name
|
||||
self.base = base
|
||||
self.colors = colors
|
||||
self.appColors = appColors
|
||||
self.wallpaper = wallpaper
|
||||
}
|
||||
|
||||
static func == (lhs: AppTheme, rhs: AppTheme) -> Bool {
|
||||
lhs.name == rhs.name &&
|
||||
lhs.colors == rhs.colors &&
|
||||
lhs.appColors == rhs.appColors &&
|
||||
lhs.wallpaper == rhs.wallpaper
|
||||
}
|
||||
|
||||
func updateFromCurrentColors() {
|
||||
objectWillChange.send()
|
||||
name = CurrentColors.name
|
||||
base = CurrentColors.base
|
||||
colors.updateColorsFrom(CurrentColors.colors)
|
||||
appColors.updateColorsFrom(CurrentColors.appColors)
|
||||
wallpaper.updateWallpaperFrom(CurrentColors.wallpaper)
|
||||
}
|
||||
}
|
||||
|
||||
struct ThemedBackground: ViewModifier {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var grouped: Bool = false
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.background(
|
||||
theme.base == DefaultTheme.SIMPLEX
|
||||
? LinearGradient(
|
||||
colors: [
|
||||
grouped
|
||||
? theme.colors.background.lighter(0.4).asGroupedBackground(theme.base.mode)
|
||||
: theme.colors.background.lighter(0.4),
|
||||
grouped
|
||||
? theme.colors.background.darker(0.4).asGroupedBackground(theme.base.mode)
|
||||
: theme.colors.background.darker(0.4)
|
||||
],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
: LinearGradient(
|
||||
colors: [],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.background(
|
||||
theme.base == DefaultTheme.SIMPLEX
|
||||
? Color.clear
|
||||
: grouped
|
||||
? theme.colors.background.asGroupedBackground(theme.base.mode)
|
||||
: theme.colors.background
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var systemInDarkThemeCurrently: Bool {
|
||||
return UITraitCollection.current.userInterfaceStyle == .dark
|
||||
}
|
||||
|
||||
func reactOnDarkThemeChanges() {
|
||||
if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME && CurrentColors.colors.isLight == systemInDarkThemeCurrently {
|
||||
// Change active colors from light to dark and back based on system theme
|
||||
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
extension ThemeWallpaper {
|
||||
public func importFromString() -> ThemeWallpaper {
|
||||
if preset == nil, let image {
|
||||
// Need to save image from string and to save its path
|
||||
if let data = Data(base64Encoded: dropImagePrefix(image)),
|
||||
let parsed = UIImage(data: data),
|
||||
let filename = saveWallpaperFile(image: parsed) {
|
||||
var copy = self
|
||||
copy.image = nil
|
||||
copy.imageFile = filename
|
||||
return copy
|
||||
} else {
|
||||
return ThemeWallpaper()
|
||||
}
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
func withFilledWallpaperBase64() -> ThemeWallpaper {
|
||||
let aw = toAppWallpaper()
|
||||
let type = aw.type
|
||||
let preset: String? = if case let WallpaperType.preset(filename, _) = type { filename } else { nil }
|
||||
let scale: Float? = if case let WallpaperType.preset(_, scale) = type { scale } else { if case let WallpaperType.image(_, scale, _) = type { scale } else { 1.0 } }
|
||||
let scaleType: WallpaperScaleType? = if case let WallpaperType.image(_, _, scaleType) = type { scaleType } else { nil }
|
||||
let image: String? = if case WallpaperType.image = type, let image = type.uiImage { resizeImageToStrSize(image, maxDataSize: 5_000_000) } else { nil }
|
||||
return ThemeWallpaper (
|
||||
preset: preset,
|
||||
scale: scale,
|
||||
scaleType: scaleType,
|
||||
background: aw.background?.toReadableHex(),
|
||||
tint: aw.tint?.toReadableHex(),
|
||||
image: image,
|
||||
imageFile: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension ThemeModeOverride {
|
||||
func removeSameColors(_ base: DefaultTheme, colorsToCompare tc: ThemeColors) -> ThemeModeOverride {
|
||||
let wallpaperType = WallpaperType.from(wallpaper) ?? WallpaperType.empty
|
||||
let w: ThemeWallpaper
|
||||
switch wallpaperType {
|
||||
case let WallpaperType.preset(filename, scale):
|
||||
let p = PresetWallpaper.from(filename)
|
||||
w = ThemeWallpaper(
|
||||
preset: filename,
|
||||
scale: scale ?? wallpaper?.scale,
|
||||
scaleType: nil,
|
||||
background: p?.background[base]?.toReadableHex(),
|
||||
tint: p?.tint[base]?.toReadableHex(),
|
||||
image: nil,
|
||||
imageFile: nil
|
||||
)
|
||||
case WallpaperType.image:
|
||||
w = ThemeWallpaper(
|
||||
preset: nil,
|
||||
scale: nil,
|
||||
scaleType: WallpaperScaleType.fill,
|
||||
background: Color.clear.toReadableHex(),
|
||||
tint: Color.clear.toReadableHex(),
|
||||
image: nil,
|
||||
imageFile: nil
|
||||
)
|
||||
default:
|
||||
w = ThemeWallpaper()
|
||||
}
|
||||
let wallpaper: ThemeWallpaper? = if let wallpaper {
|
||||
ThemeWallpaper(
|
||||
preset: wallpaper.preset,
|
||||
scale: wallpaper.scale != w.scale ? wallpaper.scale : nil,
|
||||
scaleType: wallpaper.scaleType != w.scaleType ? wallpaper.scaleType : nil,
|
||||
background: wallpaper.background != w.background ? wallpaper.background : nil,
|
||||
tint: wallpaper.tint != w.tint ? wallpaper.tint : nil,
|
||||
image: wallpaper.image,
|
||||
imageFile: wallpaper.imageFile
|
||||
)
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
return ThemeModeOverride(
|
||||
mode: self.mode,
|
||||
colors: ThemeColors(
|
||||
primary: colors.primary != tc.primary ? colors.primary : nil,
|
||||
primaryVariant: colors.primaryVariant != tc.primaryVariant ? colors.primaryVariant : nil,
|
||||
secondary: colors.secondary != tc.secondary ? colors.secondary : nil,
|
||||
secondaryVariant: colors.secondaryVariant != tc.secondaryVariant ? colors.secondaryVariant : nil,
|
||||
background: colors.background != tc.background ? colors.background : nil,
|
||||
surface: colors.surface != tc.surface ? colors.surface : nil,
|
||||
title: colors.title != tc.title ? colors.title : nil,
|
||||
primaryVariant2: colors.primaryVariant2 != tc.primaryVariant2 ? colors.primary : nil,
|
||||
sentMessage: colors.sentMessage != tc.sentMessage ? colors.sentMessage : nil,
|
||||
sentQuote: colors.sentQuote != tc.sentQuote ? colors.sentQuote : nil,
|
||||
receivedMessage: colors.receivedMessage != tc.receivedMessage ? colors.receivedMessage : nil,
|
||||
receivedQuote: colors.receivedQuote != tc.receivedQuote ? colors.receivedQuote : nil
|
||||
),
|
||||
wallpaper: wallpaper
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
//
|
||||
// ThemeManager.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Avently on 03.06.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
class ThemeManager {
|
||||
struct ActiveTheme: Equatable {
|
||||
let name: String
|
||||
let base: DefaultTheme
|
||||
let colors: Colors
|
||||
let appColors: AppColors
|
||||
var wallpaper: AppWallpaper = AppWallpaper(background: nil, tint: nil, type: .empty)
|
||||
|
||||
func toAppTheme() -> AppTheme {
|
||||
AppTheme(name: name, base: base, colors: colors, appColors: appColors, wallpaper: wallpaper)
|
||||
}
|
||||
}
|
||||
|
||||
private static func systemDarkThemeColors() -> (Colors, DefaultTheme) {
|
||||
switch systemDarkThemeDefault.get() {
|
||||
case DefaultTheme.DARK.themeName: (DarkColorPalette, DefaultTheme.DARK)
|
||||
case DefaultTheme.SIMPLEX.themeName: (SimplexColorPalette, DefaultTheme.SIMPLEX)
|
||||
case DefaultTheme.BLACK.themeName: (BlackColorPalette, DefaultTheme.BLACK)
|
||||
default: (SimplexColorPalette, DefaultTheme.SIMPLEX)
|
||||
}
|
||||
}
|
||||
|
||||
private static func nonSystemThemeName() -> String {
|
||||
let themeName = currentThemeDefault.get()
|
||||
return if themeName != DefaultTheme.SYSTEM_THEME_NAME {
|
||||
themeName
|
||||
} else {
|
||||
systemInDarkThemeCurrently ? systemDarkThemeDefault.get() : DefaultTheme.LIGHT.themeName
|
||||
}
|
||||
}
|
||||
|
||||
static func defaultActiveTheme(_ appSettingsTheme: [ThemeOverrides]) -> ThemeOverrides? {
|
||||
let nonSystemThemeName = nonSystemThemeName()
|
||||
let defaultThemeId = currentThemeIdsDefault.get()[nonSystemThemeName]
|
||||
return appSettingsTheme.getTheme(defaultThemeId)
|
||||
}
|
||||
|
||||
static func defaultActiveTheme(_ perUserTheme: ThemeModeOverrides?, _ appSettingsTheme: [ThemeOverrides]) -> ThemeModeOverride {
|
||||
let perUserTheme = !CurrentColors.colors.isLight ? perUserTheme?.dark : perUserTheme?.light
|
||||
if let perUserTheme {
|
||||
return perUserTheme
|
||||
}
|
||||
let defaultTheme = defaultActiveTheme(appSettingsTheme)
|
||||
return ThemeModeOverride(mode: CurrentColors.base.mode, colors: defaultTheme?.colors ?? ThemeColors(), wallpaper: defaultTheme?.wallpaper)
|
||||
}
|
||||
|
||||
static func currentColors(_ themeOverridesForType: WallpaperType?, _ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverrides?, _ appSettingsTheme: [ThemeOverrides]) -> ActiveTheme {
|
||||
let themeName = currentThemeDefault.get()
|
||||
let nonSystemThemeName = nonSystemThemeName()
|
||||
let defaultTheme = defaultActiveTheme(appSettingsTheme)
|
||||
|
||||
let baseTheme = switch nonSystemThemeName {
|
||||
case DefaultTheme.LIGHT.themeName: ActiveTheme(name: DefaultTheme.LIGHT.themeName, base: DefaultTheme.LIGHT, colors: LightColorPalette.clone(), appColors: LightColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.LIGHT)))
|
||||
case DefaultTheme.DARK.themeName: ActiveTheme(name: DefaultTheme.DARK.themeName, base: DefaultTheme.DARK, colors: DarkColorPalette.clone(), appColors: DarkColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.DARK)))
|
||||
case DefaultTheme.SIMPLEX.themeName: ActiveTheme(name: DefaultTheme.SIMPLEX.themeName, base: DefaultTheme.SIMPLEX, colors: SimplexColorPalette.clone(), appColors: SimplexColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.SIMPLEX)))
|
||||
case DefaultTheme.BLACK.themeName: ActiveTheme(name: DefaultTheme.BLACK.themeName, base: DefaultTheme.BLACK, colors: BlackColorPalette.clone(), appColors: BlackColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.BLACK)))
|
||||
default: ActiveTheme(name: DefaultTheme.LIGHT.themeName, base: DefaultTheme.LIGHT, colors: LightColorPalette.clone(), appColors: LightColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.LIGHT)))
|
||||
}
|
||||
|
||||
let perUserTheme = baseTheme.colors.isLight ? perUserTheme?.light : perUserTheme?.dark
|
||||
let theme = appSettingsTheme.sameTheme(themeOverridesForType ?? perChatTheme?.type ?? perUserTheme?.type ?? defaultTheme?.wallpaper?.toAppWallpaper().type, nonSystemThemeName) ?? defaultTheme
|
||||
|
||||
if theme == nil && perUserTheme == nil && perChatTheme == nil && themeOverridesForType == nil {
|
||||
return ActiveTheme(name: themeName, base: baseTheme.base, colors: baseTheme.colors, appColors: baseTheme.appColors, wallpaper: baseTheme.wallpaper)
|
||||
}
|
||||
let presetWallpaperTheme: ThemeColors? = if let themeOverridesForType, case let WallpaperType.preset(filename, _) = themeOverridesForType {
|
||||
PresetWallpaper.from(filename)?.colors[baseTheme.base]
|
||||
} else if let wallpaper = perChatTheme?.wallpaper {
|
||||
if let preset = wallpaper.preset { PresetWallpaper.from(preset)?.colors[baseTheme.base] } else { nil }
|
||||
} else if let wallpaper = perUserTheme?.wallpaper {
|
||||
if let preset = wallpaper.preset { PresetWallpaper.from(preset)?.colors[baseTheme.base] } else { nil }
|
||||
} else {
|
||||
if let preset = theme?.wallpaper?.preset { PresetWallpaper.from(preset)?.colors[baseTheme.base] } else { nil }
|
||||
}
|
||||
|
||||
let themeOrEmpty = theme ?? ThemeOverrides(base: baseTheme.base)
|
||||
let colors = themeOrEmpty.toColors(themeOrEmpty.base, perChatTheme?.colors, perUserTheme?.colors, presetWallpaperTheme)
|
||||
return ActiveTheme(
|
||||
name: themeName,
|
||||
base: baseTheme.base,
|
||||
colors: colors,
|
||||
appColors: themeOrEmpty.toAppColors(themeOrEmpty.base, perChatTheme?.colors, perChatTheme?.type, perUserTheme?.colors, perUserTheme?.type, presetWallpaperTheme),
|
||||
wallpaper: themeOrEmpty.toAppWallpaper(themeOverridesForType, perChatTheme, perUserTheme, colors.background)
|
||||
)
|
||||
}
|
||||
|
||||
static func currentThemeOverridesForExport(_ themeOverridesForType: WallpaperType?, _ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverrides?) -> ThemeOverrides {
|
||||
let current = currentColors(themeOverridesForType, perChatTheme, perUserTheme, themeOverridesDefault.get())
|
||||
let wType = current.wallpaper.type
|
||||
let wBackground = current.wallpaper.background
|
||||
let wTint = current.wallpaper.tint
|
||||
let w: ThemeWallpaper? = if case WallpaperType.empty = wType {
|
||||
nil
|
||||
} else {
|
||||
ThemeWallpaper.from(wType, wBackground?.toReadableHex(), wTint?.toReadableHex()).withFilledWallpaperBase64()
|
||||
}
|
||||
return ThemeOverrides(
|
||||
themeId: "",
|
||||
base: current.base,
|
||||
colors: ThemeColors.from(current.colors, current.appColors),
|
||||
wallpaper: w
|
||||
)
|
||||
}
|
||||
|
||||
static func applyTheme(_ theme: String) {
|
||||
currentThemeDefault.set(theme)
|
||||
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
AppTheme.shared.updateFromCurrentColors()
|
||||
let tint = UIColor(CurrentColors.colors.primary)
|
||||
if SceneDelegate.windowStatic?.tintColor != tint {
|
||||
SceneDelegate.windowStatic?.tintColor = tint
|
||||
}
|
||||
// applyNavigationBarColors(CurrentColors.toAppTheme())
|
||||
}
|
||||
|
||||
static func adjustWindowStyle() {
|
||||
let style = switch currentThemeDefault.get() {
|
||||
case DefaultTheme.LIGHT.themeName: UIUserInterfaceStyle.light
|
||||
case DefaultTheme.SYSTEM_THEME_NAME: UIUserInterfaceStyle.unspecified
|
||||
default: UIUserInterfaceStyle.dark
|
||||
}
|
||||
if SceneDelegate.windowStatic?.overrideUserInterfaceStyle != style {
|
||||
SceneDelegate.windowStatic?.overrideUserInterfaceStyle = style
|
||||
}
|
||||
}
|
||||
|
||||
// static func applyNavigationBarColors(_ theme: AppTheme) {
|
||||
// let baseColors = switch theme.base {
|
||||
// case DefaultTheme.LIGHT: LightColorPaletteApp
|
||||
// case DefaultTheme.DARK: DarkColorPaletteApp
|
||||
// case DefaultTheme.SIMPLEX: SimplexColorPaletteApp
|
||||
// case DefaultTheme.BLACK: BlackColorPaletteApp
|
||||
// }
|
||||
// let isDefaultColor = baseColors.title == theme.appColors.title
|
||||
//
|
||||
// let title = UIColor(theme.appColors.title)
|
||||
// if !isDefaultColor && UINavigationBar.appearance().titleTextAttributes?.first as? UIColor != title {
|
||||
// UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: title]
|
||||
// UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: title]
|
||||
// } else {
|
||||
// UINavigationBar.appearance().titleTextAttributes = nil
|
||||
// UINavigationBar.appearance().largeTitleTextAttributes = nil
|
||||
// }
|
||||
// }
|
||||
|
||||
static func changeDarkTheme(_ theme: String) {
|
||||
systemDarkThemeDefault.set(theme)
|
||||
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
AppTheme.shared.updateFromCurrentColors()
|
||||
}
|
||||
|
||||
static func saveAndApplyThemeColor(_ baseTheme: DefaultTheme, _ name: ThemeColor, _ color: Color? = nil, _ pref: CodableDefault<[ThemeOverrides]>? = nil) {
|
||||
let nonSystemThemeName = baseTheme.themeName
|
||||
let pref = pref ?? themeOverridesDefault
|
||||
let overrides = pref.get()
|
||||
let themeId = currentThemeIdsDefault.get()[nonSystemThemeName]
|
||||
let prevValue = overrides.getTheme(themeId) ?? ThemeOverrides(base: baseTheme)
|
||||
pref.set(overrides.replace(prevValue.withUpdatedColor(name, color?.toReadableHex())))
|
||||
var themeIds = currentThemeIdsDefault.get()
|
||||
themeIds[nonSystemThemeName] = prevValue.themeId
|
||||
currentThemeIdsDefault.set(themeIds)
|
||||
applyTheme(currentThemeDefault.get())
|
||||
}
|
||||
|
||||
static func applyThemeColor(name: ThemeColor, color: Color? = nil, pref: Binding<ThemeModeOverride>) {
|
||||
pref.wrappedValue = pref.wrappedValue.withUpdatedColor(name, color?.toReadableHex())
|
||||
}
|
||||
|
||||
static func saveAndApplyWallpaper(_ baseTheme: DefaultTheme, _ type: WallpaperType?, _ pref: CodableDefault<[ThemeOverrides]>?) {
|
||||
let nonSystemThemeName = baseTheme.themeName
|
||||
let pref = pref ?? themeOverridesDefault
|
||||
let overrides = pref.get()
|
||||
let theme = overrides.sameTheme(type, baseTheme.themeName)
|
||||
var prevValue = theme ?? ThemeOverrides(base: baseTheme)
|
||||
prevValue.wallpaper = if let type {
|
||||
if case WallpaperType.empty = type {
|
||||
nil as ThemeWallpaper?
|
||||
} else {
|
||||
ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint)
|
||||
}
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
pref.set(overrides.replace(prevValue))
|
||||
var themeIds = currentThemeIdsDefault.get()
|
||||
themeIds[nonSystemThemeName] = prevValue.themeId
|
||||
currentThemeIdsDefault.set(themeIds)
|
||||
applyTheme(nonSystemThemeName)
|
||||
}
|
||||
|
||||
static func copyFromSameThemeOverrides(_ type: WallpaperType?, _ lowerLevelOverride: ThemeModeOverride?, _ pref: Binding<ThemeModeOverride>) -> Bool {
|
||||
let overrides = themeOverridesDefault.get()
|
||||
let sameWallpaper: ThemeWallpaper? = if let wallpaper = lowerLevelOverride?.wallpaper, lowerLevelOverride?.type?.sameType(type) == true {
|
||||
wallpaper
|
||||
} else {
|
||||
overrides.sameTheme(type, CurrentColors.base.themeName)?.wallpaper
|
||||
}
|
||||
guard let sameWallpaper else {
|
||||
if let type {
|
||||
var w: ThemeWallpaper = ThemeWallpaper.from(type, nil, nil)
|
||||
w.scale = nil
|
||||
w.scaleType = nil
|
||||
w.background = nil
|
||||
w.tint = nil
|
||||
pref.wrappedValue = ThemeModeOverride(mode: CurrentColors.base.mode, wallpaper: w)
|
||||
} else {
|
||||
// Make an empty wallpaper to override any top level ones
|
||||
pref.wrappedValue = ThemeModeOverride(mode: CurrentColors.base.mode, wallpaper: ThemeWallpaper())
|
||||
}
|
||||
return true
|
||||
}
|
||||
var type = sameWallpaper.toAppWallpaper().type
|
||||
if case let WallpaperType.image(filename, scale, scaleType) = type, sameWallpaper.imageFile == filename {
|
||||
// same image file. Needs to be copied first in order to be able to remove the file once it's not needed anymore without affecting main theme override
|
||||
if let filename = saveWallpaperFile(url: getWallpaperFilePath(filename)) {
|
||||
type = WallpaperType.image(filename, scale, scaleType)
|
||||
} else {
|
||||
logger.error("Error while copying wallpaper from global overrides to chat overrides")
|
||||
return false
|
||||
}
|
||||
}
|
||||
var prevValue = pref.wrappedValue
|
||||
var w = ThemeWallpaper.from(type, nil, nil)
|
||||
w.scale = nil
|
||||
w.scaleType = nil
|
||||
w.background = nil
|
||||
w.tint = nil
|
||||
prevValue.colors = ThemeColors()
|
||||
prevValue.wallpaper = w
|
||||
pref.wrappedValue = prevValue
|
||||
return true
|
||||
}
|
||||
|
||||
static func applyWallpaper(_ type: WallpaperType?, _ pref: Binding<ThemeModeOverride>) {
|
||||
var prevValue = pref.wrappedValue
|
||||
prevValue.wallpaper = if let type {
|
||||
ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint)
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
pref.wrappedValue = prevValue
|
||||
}
|
||||
|
||||
static func saveAndApplyThemeOverrides(_ theme: ThemeOverrides, _ pref: CodableDefault<[ThemeOverrides]>? = nil) {
|
||||
let wallpaper = theme.wallpaper?.importFromString()
|
||||
let nonSystemThemeName = theme.base.themeName
|
||||
let pref: CodableDefault<[ThemeOverrides]> = pref ?? themeOverridesDefault
|
||||
let overrides = pref.get()
|
||||
var prevValue = overrides.getTheme(nil, wallpaper?.toAppWallpaper().type, theme.base) ?? ThemeOverrides(base: theme.base)
|
||||
if let imageFile = prevValue.wallpaper?.imageFile {
|
||||
try? FileManager.default.removeItem(at: getWallpaperFilePath(imageFile))
|
||||
}
|
||||
prevValue.base = theme.base
|
||||
prevValue.colors = theme.colors
|
||||
prevValue.wallpaper = wallpaper
|
||||
pref.set(overrides.replace(prevValue))
|
||||
currentThemeDefault.set(nonSystemThemeName)
|
||||
var currentThemeIds = currentThemeIdsDefault.get()
|
||||
currentThemeIds[nonSystemThemeName] = prevValue.themeId
|
||||
currentThemeIdsDefault.set(currentThemeIds)
|
||||
applyTheme(nonSystemThemeName)
|
||||
}
|
||||
|
||||
static func resetAllThemeColors(_ pref: CodableDefault<[ThemeOverrides]>? = nil) {
|
||||
let nonSystemThemeName = nonSystemThemeName()
|
||||
let pref: CodableDefault<[ThemeOverrides]> = pref ?? themeOverridesDefault
|
||||
let overrides = pref.get()
|
||||
guard let themeId = currentThemeIdsDefault.get()[nonSystemThemeName],
|
||||
var prevValue = overrides.getTheme(themeId)
|
||||
else { return }
|
||||
prevValue.colors = ThemeColors()
|
||||
prevValue.wallpaper?.background = nil
|
||||
prevValue.wallpaper?.tint = nil
|
||||
pref.set(overrides.replace(prevValue))
|
||||
applyTheme(currentThemeDefault.get())
|
||||
}
|
||||
|
||||
static func resetAllThemeColors(_ pref: Binding<ThemeModeOverride>) {
|
||||
var prevValue = pref.wrappedValue
|
||||
prevValue.colors = ThemeColors()
|
||||
prevValue.wallpaper?.background = nil
|
||||
prevValue.wallpaper?.tint = nil
|
||||
pref.wrappedValue = prevValue
|
||||
}
|
||||
|
||||
static func removeTheme(_ themeId: String?) {
|
||||
var themes = themeOverridesDefault.get().map { $0 }
|
||||
themes.removeAll(where: { $0.themeId == themeId })
|
||||
themeOverridesDefault.set(themes)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// MPVolumeView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Avently on 24.04.2024.
|
||||
// Created by Stanislav on 24.04.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import SimpleXChat
|
||||
|
||||
struct IncomingCallView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var cc = CallController.shared
|
||||
|
||||
var body: some View {
|
||||
@@ -44,7 +43,7 @@ struct IncomingCallView: View {
|
||||
cc.endCall(invitation: invitation)
|
||||
}
|
||||
|
||||
callButton("Ignore", "multiply", .primary) {
|
||||
callButton("Ignore", "multiply", .accentColor) {
|
||||
cc.activeCallInvitation = nil
|
||||
}
|
||||
|
||||
@@ -64,7 +63,7 @@ struct IncomingCallView: View {
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.frame(maxWidth: .infinity)
|
||||
.modifier(ThemedBackground())
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.onAppear { dismissAllSheets() }
|
||||
}
|
||||
|
||||
@@ -77,7 +76,7 @@ struct IncomingCallView: View {
|
||||
.frame(width: 24, height: 24)
|
||||
Text(text)
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(minWidth: 44)
|
||||
})
|
||||
|
||||
@@ -9,9 +9,10 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
let chatImageColorLight = Color(red: 0.9, green: 0.9, blue: 0.9)
|
||||
let chatImageColorDark = Color(red: 0.2, green: 0.2, blue: 0.2)
|
||||
struct ChatInfoToolbar: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
var imageSize: CGFloat = 32
|
||||
|
||||
@@ -25,7 +26,9 @@ struct ChatInfoToolbar: View {
|
||||
ChatInfoImage(
|
||||
chat: chat,
|
||||
size: imageSize,
|
||||
color: Color(uiColor: .tertiaryLabel)
|
||||
color: colorScheme == .dark
|
||||
? chatImageColorDark
|
||||
: chatImageColorLight
|
||||
)
|
||||
.padding(.trailing, 4)
|
||||
VStack {
|
||||
@@ -38,14 +41,14 @@ struct ChatInfoToolbar: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.foregroundColor(theme.colors.onBackground)
|
||||
.foregroundColor(.primary)
|
||||
.frame(width: 220)
|
||||
}
|
||||
|
||||
private var contactVerifiedShield: Text {
|
||||
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.baselineOffset(1)
|
||||
.kerning(-2)
|
||||
}
|
||||
@@ -54,6 +57,5 @@ struct ChatInfoToolbar: View {
|
||||
struct ChatInfoToolbar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ChatInfoToolbar(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []))
|
||||
.environmentObject(CurrentColors.toAppTheme())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,14 +36,14 @@ func localizedInfoRow(_ title: LocalizedStringKey, _ value: LocalizedStringKey)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func smpServers(_ title: LocalizedStringKey, _ servers: [String], _ secondaryColor: Color) -> some View {
|
||||
@ViewBuilder func smpServers(_ title: LocalizedStringKey, _ servers: [String]) -> some View {
|
||||
if servers.count > 0 {
|
||||
HStack {
|
||||
Text(title).frame(width: 120, alignment: .leading)
|
||||
Button(serverHost(servers[0])) {
|
||||
UIPasteboard.general.string = servers.joined(separator: ";")
|
||||
}
|
||||
.foregroundColor(secondaryColor)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,6 @@ enum SendReceipts: Identifiable, Hashable {
|
||||
|
||||
struct ChatInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@ObservedObject var chat: Chat
|
||||
@State var contact: Contact
|
||||
@@ -144,7 +143,7 @@ struct ChatInfoView: View {
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
if let customUserProfile = customUserProfile {
|
||||
Section(header: Text("Incognito").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Incognito") {
|
||||
HStack {
|
||||
Text("Your random profile")
|
||||
Spacer()
|
||||
@@ -162,11 +161,6 @@ struct ChatInfoView: View {
|
||||
connStats.ratchetSyncAllowed {
|
||||
synchronizeConnectionButton()
|
||||
}
|
||||
NavigationLink {
|
||||
ChatWallpaperEditorSheet(chat: chat)
|
||||
} label: {
|
||||
Label("Chat theme", systemImage: "photo")
|
||||
}
|
||||
// } else if developerTools {
|
||||
// synchronizeConnectionButtonForce()
|
||||
// }
|
||||
@@ -189,15 +183,13 @@ struct ChatInfoView: View {
|
||||
}
|
||||
} header: {
|
||||
Text("Address")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text("You can share this address with your contacts to let them connect with **\(contact.displayName)**.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
if contact.ready && contact.active {
|
||||
Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Servers") {
|
||||
networkStatusRow()
|
||||
.onTapGesture {
|
||||
alert = .networkStatusAlert
|
||||
@@ -219,8 +211,8 @@ struct ChatInfoView: View {
|
||||
|| connStats.ratchetSyncSendProhibited
|
||||
)
|
||||
}
|
||||
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }, theme.colors.secondary)
|
||||
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }, theme.colors.secondary)
|
||||
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer })
|
||||
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,7 +223,7 @@ struct ChatInfoView: View {
|
||||
}
|
||||
|
||||
if developerTools {
|
||||
Section(header: Text("For console").foregroundColor(theme.colors.secondary)) {
|
||||
Section(header: Text("For console")) {
|
||||
infoRow("Local name", chat.chatInfo.localDisplayName)
|
||||
infoRow("Database ID", "\(chat.chatInfo.apiId)")
|
||||
Button ("Debug delivery") {
|
||||
@@ -249,7 +241,6 @@ struct ChatInfoView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
@@ -301,7 +292,7 @@ struct ChatInfoView: View {
|
||||
if contact.verified {
|
||||
(
|
||||
Text(Image(systemName: "checkmark.shield"))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.title2)
|
||||
+ Text(" ")
|
||||
+ Text(contact.profile.displayName)
|
||||
@@ -341,7 +332,7 @@ struct ChatInfoView: View {
|
||||
setContactAlias()
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
private func setContactAlias() {
|
||||
@@ -379,7 +370,6 @@ struct ChatInfoView: View {
|
||||
)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("Security code")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Label(
|
||||
contact.verified ? "View security code" : "Verify security code",
|
||||
@@ -396,7 +386,6 @@ struct ChatInfoView: View {
|
||||
currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences)
|
||||
)
|
||||
.navigationBarTitle("Contact preferences")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
Label("Contact preferences", systemImage: "switch.2")
|
||||
@@ -445,11 +434,11 @@ struct ChatInfoView: View {
|
||||
HStack {
|
||||
Text("Network status")
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.foregroundColor(.accentColor)
|
||||
.font(.system(size: 14))
|
||||
Spacer()
|
||||
Text(chatModel.contactNetworkStatus(contact).statusString)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
serverImage()
|
||||
}
|
||||
}
|
||||
@@ -457,7 +446,7 @@ struct ChatInfoView: View {
|
||||
private func serverImage() -> some View {
|
||||
let status = chatModel.contactNetworkStatus(contact)
|
||||
return Image(systemName: status.imageName)
|
||||
.foregroundColor(status == .connected ? .green : theme.colors.secondary)
|
||||
.foregroundColor(status == .connected ? .green : .secondary)
|
||||
.font(.system(size: 12))
|
||||
}
|
||||
|
||||
@@ -576,148 +565,6 @@ struct ChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatWallpaperEditorSheet: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@State private var globalThemeUsed: Bool = false
|
||||
@State var chat: Chat
|
||||
@State private var themes: ThemeModeOverrides
|
||||
|
||||
init(chat: Chat) {
|
||||
self.chat = chat
|
||||
self.themes = if case let ChatInfo.direct(contact) = chat.chatInfo, let uiThemes = contact.uiThemes {
|
||||
uiThemes
|
||||
} else if case let ChatInfo.group(groupInfo) = chat.chatInfo, let uiThemes = groupInfo.uiThemes {
|
||||
uiThemes
|
||||
} else {
|
||||
ThemeModeOverrides()
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let preferred = themes.preferredMode(!theme.colors.isLight)
|
||||
let initialTheme = preferred ?? ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
ChatWallpaperEditor(
|
||||
initialTheme: initialTheme,
|
||||
themeModeOverride: initialTheme,
|
||||
applyToMode: themes.light == themes.dark ? nil : initialTheme.mode,
|
||||
globalThemeUsed: $globalThemeUsed,
|
||||
save: { applyToMode, newTheme in
|
||||
await save(applyToMode, newTheme, $chat)
|
||||
}
|
||||
)
|
||||
.navigationTitle("Chat theme")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.onAppear {
|
||||
globalThemeUsed = preferred == nil
|
||||
}
|
||||
.onChange(of: theme.base.mode) { _ in
|
||||
globalThemeUsed = themesFromChat(chat).preferredMode(!theme.colors.isLight) == nil
|
||||
}
|
||||
.onChange(of: ChatModel.shared.chatId) { _ in
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private func themesFromChat(_ chat: Chat) -> ThemeModeOverrides {
|
||||
if case let ChatInfo.direct(contact) = chat.chatInfo, let uiThemes = contact.uiThemes {
|
||||
uiThemes
|
||||
} else if case let ChatInfo.group(groupInfo) = chat.chatInfo, let uiThemes = groupInfo.uiThemes {
|
||||
uiThemes
|
||||
} else {
|
||||
ThemeModeOverrides()
|
||||
}
|
||||
}
|
||||
|
||||
private static var updateBackendTask: Task = Task {}
|
||||
private func save(
|
||||
_ applyToMode: DefaultThemeMode?,
|
||||
_ newTheme: ThemeModeOverride?,
|
||||
_ chat: Binding<Chat>
|
||||
) async {
|
||||
let unchangedThemes: ThemeModeOverrides = themesFromChat(chat.wrappedValue)
|
||||
var wallpaperFiles = Set([unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile])
|
||||
var changedThemes: ThemeModeOverrides? = unchangedThemes
|
||||
let light: ThemeModeOverride? = if let newTheme {
|
||||
ThemeModeOverride(mode: DefaultThemeMode.light, colors: newTheme.colors, wallpaper: newTheme.wallpaper?.withFilledWallpaperPath())
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
let dark: ThemeModeOverride? = if let newTheme {
|
||||
ThemeModeOverride(mode: DefaultThemeMode.dark, colors: newTheme.colors, wallpaper: newTheme.wallpaper?.withFilledWallpaperPath())
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
|
||||
if let applyToMode {
|
||||
switch applyToMode {
|
||||
case DefaultThemeMode.light:
|
||||
changedThemes?.light = light
|
||||
case DefaultThemeMode.dark:
|
||||
changedThemes?.dark = dark
|
||||
}
|
||||
} else {
|
||||
changedThemes?.light = light
|
||||
changedThemes?.dark = dark
|
||||
}
|
||||
if changedThemes?.light != nil || changedThemes?.dark != nil {
|
||||
let light = changedThemes?.light
|
||||
let dark = changedThemes?.dark
|
||||
let currentMode = CurrentColors.base.mode
|
||||
// same image file for both modes, copy image to make them as different files
|
||||
if var light, var dark, let lightWallpaper = light.wallpaper, let darkWallpaper = dark.wallpaper, let lightImageFile = lightWallpaper.imageFile, let darkImageFile = darkWallpaper.imageFile, lightWallpaper.imageFile == darkWallpaper.imageFile {
|
||||
let imageFile = if currentMode == DefaultThemeMode.light {
|
||||
darkImageFile
|
||||
} else {
|
||||
lightImageFile
|
||||
}
|
||||
let filePath = saveWallpaperFile(url: getWallpaperFilePath(imageFile))
|
||||
if currentMode == DefaultThemeMode.light {
|
||||
dark.wallpaper?.imageFile = filePath
|
||||
changedThemes = ThemeModeOverrides(light: changedThemes?.light, dark: dark)
|
||||
} else {
|
||||
light.wallpaper?.imageFile = filePath
|
||||
changedThemes = ThemeModeOverrides(light: light, dark: changedThemes?.dark)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
changedThemes = nil
|
||||
}
|
||||
wallpaperFiles.remove(changedThemes?.light?.wallpaper?.imageFile)
|
||||
wallpaperFiles.remove(changedThemes?.dark?.wallpaper?.imageFile)
|
||||
wallpaperFiles.forEach(removeWallpaperFile)
|
||||
|
||||
let changedThemesConstant = changedThemes
|
||||
ChatWallpaperEditorSheet.updateBackendTask.cancel()
|
||||
ChatWallpaperEditorSheet.updateBackendTask = Task {
|
||||
do {
|
||||
try await Task.sleep(nanoseconds: 300_000000)
|
||||
if await apiSetChatUIThemes(chatId: chat.id, themes: changedThemesConstant) {
|
||||
if case var ChatInfo.direct(contact) = chat.wrappedValue.chatInfo {
|
||||
contact.uiThemes = changedThemesConstant
|
||||
await MainActor.run {
|
||||
ChatModel.shared.updateChatInfo(ChatInfo.direct(contact: contact))
|
||||
chat.wrappedValue = Chat.init(chatInfo: ChatInfo.direct(contact: contact))
|
||||
themes = themesFromChat(chat.wrappedValue)
|
||||
}
|
||||
} else if case var ChatInfo.group(groupInfo) = chat.wrappedValue.chatInfo {
|
||||
groupInfo.uiThemes = changedThemesConstant
|
||||
|
||||
await MainActor.run {
|
||||
ChatModel.shared.updateChatInfo(ChatInfo.group(groupInfo: groupInfo))
|
||||
chat.wrappedValue = Chat.init(chatInfo: ChatInfo.group(groupInfo: groupInfo))
|
||||
themes = themesFromChat(chat.wrappedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// canceled task
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func switchAddressAlert(_ switchAddress: @escaping () -> Void) -> Alert {
|
||||
Alert(
|
||||
title: Text("Change receiving address?"),
|
||||
|
||||
@@ -11,7 +11,6 @@ import SimpleXChat
|
||||
|
||||
struct CICallItemView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var status: CICallStatus
|
||||
@@ -23,7 +22,7 @@ struct CICallItemView: View {
|
||||
switch status {
|
||||
case .pending:
|
||||
if sent {
|
||||
Image(systemName: "phone.arrow.up.right").foregroundColor(theme.colors.secondary)
|
||||
Image(systemName: "phone.arrow.up.right").foregroundColor(.secondary)
|
||||
} else {
|
||||
acceptCallButton()
|
||||
}
|
||||
@@ -36,7 +35,7 @@ struct CICallItemView: View {
|
||||
case .error: missedCallIcon(sent).foregroundColor(.orange)
|
||||
}
|
||||
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary, showStatus: false, showEdited: false)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, showStatus: false, showEdited: false)
|
||||
.padding(.bottom, 8)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
@@ -53,7 +52,7 @@ struct CICallItemView: View {
|
||||
@ViewBuilder private func endedCallIcon(_ sent: Bool) -> some View {
|
||||
HStack {
|
||||
Image(systemName: "phone.down")
|
||||
Text(durationText(duration)).foregroundColor(theme.colors.secondary)
|
||||
Text(durationText(duration)).foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +70,7 @@ struct CICallItemView: View {
|
||||
Label("Answer call", systemImage: "phone.arrow.down.left")
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "phone.arrow.down.left").foregroundColor(theme.colors.secondary)
|
||||
Image(systemName: "phone.arrow.down.left").foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import SimpleXChat
|
||||
struct CIChatFeatureView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
@Binding var revealed: Bool
|
||||
var feature: Feature
|
||||
@@ -67,10 +66,10 @@ struct CIChatFeatureView: View {
|
||||
|
||||
private func featureInfo(_ ci: ChatItem) -> FeatureInfo? {
|
||||
switch ci.content {
|
||||
case let .rcvChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor(theme.colors.secondary), param)
|
||||
case let .sndChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor(theme.colors.secondary), param)
|
||||
case let .rcvGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary), param)
|
||||
case let .sndGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary), param)
|
||||
case let .rcvChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor, param)
|
||||
case let .sndChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor, param)
|
||||
case let .rcvGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor, param)
|
||||
case let .sndGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor, param)
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
@@ -82,7 +81,7 @@ struct CIChatFeatureView: View {
|
||||
if let param = f.param {
|
||||
HStack {
|
||||
i
|
||||
chatEventText(Text(param), theme.colors.secondary).lineLimit(1)
|
||||
chatEventText(Text(param)).lineLimit(1)
|
||||
}
|
||||
} else {
|
||||
i
|
||||
@@ -94,7 +93,7 @@ struct CIChatFeatureView: View {
|
||||
Image(systemName: icon ?? feature.iconFilled)
|
||||
.foregroundColor(iconColor)
|
||||
.scaleEffect(feature.iconScale)
|
||||
chatEventText(chatItem, theme.colors.secondary)
|
||||
chatEventText(chatItem)
|
||||
}
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 4)
|
||||
@@ -105,6 +104,6 @@ struct CIChatFeatureView: View {
|
||||
struct CIChatFeatureView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let enabled = FeatureEnabled(forUser: false, forContact: false)
|
||||
CIChatFeatureView(chat: Chat.sampleData, chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), revealed: Binding.constant(true), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor(.secondary))
|
||||
CIChatFeatureView(chat: Chat.sampleData, chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), revealed: Binding.constant(true), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import SimpleXChat
|
||||
|
||||
struct CIFeaturePreferenceView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
var feature: ChatFeature
|
||||
var allowed: FeatureAllowed
|
||||
@@ -20,7 +19,7 @@ struct CIFeaturePreferenceView: View {
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
Image(systemName: feature.icon)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.scaleEffect(feature.iconScale)
|
||||
if let ct = chat.chatInfo.contact,
|
||||
allowed != .no && ct.allowsFeature(feature) && !ct.userAllowsFeature(feature) {
|
||||
@@ -41,17 +40,17 @@ struct CIFeaturePreferenceView: View {
|
||||
private func featurePreferenceView(acceptText: LocalizedStringKey? = nil) -> some View {
|
||||
var r = Text(CIContent.preferenceText(feature, allowed, param) + " ")
|
||||
.fontWeight(.light)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
if let acceptText {
|
||||
r = r
|
||||
+ Text(acceptText)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.foregroundColor(.accentColor)
|
||||
+ Text(" ")
|
||||
}
|
||||
r = r + chatItem.timestampText
|
||||
.fontWeight(.light)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
return r.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CIFileView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
let file: CIFile?
|
||||
let edited: Bool
|
||||
|
||||
@@ -30,12 +30,12 @@ struct CIFileView: View {
|
||||
Text(file.fileName)
|
||||
.lineLimit(1)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(theme.colors.onBackground)
|
||||
.foregroundColor(.primary)
|
||||
Text(prettyFileSize + metaReserve)
|
||||
.font(.caption)
|
||||
.lineLimit(1)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} else {
|
||||
Text(metaReserve)
|
||||
@@ -170,7 +170,7 @@ struct CIFileView: View {
|
||||
case .sndWarning: fileIcon("doc.fill", innerIcon: "exclamationmark.triangle.fill", innerIconSize: 10)
|
||||
case .rcvInvitation:
|
||||
if fileSizeValid(file) {
|
||||
fileIcon("arrow.down.doc.fill", color: theme.colors.primary)
|
||||
fileIcon("arrow.down.doc.fill", color: .accentColor)
|
||||
} else {
|
||||
fileIcon("doc.fill", color: .orange, innerIcon: "exclamationmark", innerIconSize: 12)
|
||||
}
|
||||
@@ -182,7 +182,7 @@ struct CIFileView: View {
|
||||
progressView()
|
||||
}
|
||||
case .rcvAborted:
|
||||
fileIcon("doc.fill", color: theme.colors.primary, innerIcon: "exclamationmark.arrow.circlepath", innerIconSize: 12)
|
||||
fileIcon("doc.fill", color: .accentColor, innerIcon: "exclamationmark.arrow.circlepath", innerIconSize: 12)
|
||||
case .rcvComplete: fileIcon("doc.fill")
|
||||
case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
case .rcvError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
|
||||
@@ -11,7 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CIGroupInvitationView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var groupInvitation: CIGroupInvitation
|
||||
@@ -42,7 +42,7 @@ struct CIGroupInvitationView: View {
|
||||
.overlay(DetermineWidth())
|
||||
(
|
||||
Text(chatIncognito ? "Tap to join incognito" : "Tap to join")
|
||||
.foregroundColor(inProgress ? theme.colors.secondary : chatIncognito ? .indigo : theme.colors.primary)
|
||||
.foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : .accentColor)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
|
||||
@@ -65,11 +65,12 @@ struct CIGroupInvitationView: View {
|
||||
}
|
||||
}
|
||||
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary, showStatus: false, showEdited: false)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, showStatus: false, showEdited: false)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(chatItemFrameColor(chatItem, theme))
|
||||
.background(chatItemFrameColor(chatItem, colorScheme))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
|
||||
.onChange(of: inProgress) { inProgress in
|
||||
@@ -98,7 +99,7 @@ struct CIGroupInvitationView: View {
|
||||
private func groupInfoView(_ action: Bool) -> some View {
|
||||
var color: Color
|
||||
if action && !inProgress {
|
||||
color = chatIncognito ? .indigo : theme.colors.primary
|
||||
color = chatIncognito ? .indigo : .accentColor
|
||||
} else {
|
||||
color = Color(uiColor: .tertiaryLabel)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CIImageView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
let chatItem: ChatItem
|
||||
var preview: UIImage?
|
||||
let maxWidth: CGFloat
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CIInvalidJSONView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var json: String
|
||||
@State private var showJSON = false
|
||||
|
||||
@@ -22,6 +21,7 @@ struct CIInvalidJSONView: View {
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
.onTapGesture { showJSON = true }
|
||||
.appSheet(isPresented: $showJSON) {
|
||||
@@ -44,7 +44,6 @@ func invalidJSONView(_ json: String) -> some View {
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding()
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
struct CIInvalidJSONView_Previews: PreviewProvider {
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct CILinkView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
let linkPreview: LinkPreview
|
||||
|
||||
var body: some View {
|
||||
@@ -32,7 +32,7 @@ struct CILinkView: View {
|
||||
Text(linkPreview.uri.absoluteString)
|
||||
.font(.caption)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
@@ -11,7 +11,6 @@ import SimpleXChat
|
||||
|
||||
struct CIMemberCreatedContactView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
|
||||
var body: some View {
|
||||
@@ -44,12 +43,12 @@ struct CIMemberCreatedContactView: View {
|
||||
r = r
|
||||
+ Text(openText)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.foregroundColor(.accentColor)
|
||||
+ Text(" ")
|
||||
}
|
||||
r = r + chatItem.timestampText
|
||||
.fontWeight(.light)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
return r.font(.caption)
|
||||
}
|
||||
|
||||
@@ -57,11 +56,11 @@ struct CIMemberCreatedContactView: View {
|
||||
if let member = chatItem.memberDisplayName {
|
||||
return Text(member + " " + chatItem.content.text + " ")
|
||||
.fontWeight(.light)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
return Text(chatItem.content.text + " ")
|
||||
.fontWeight(.light)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,8 @@ import SimpleXChat
|
||||
|
||||
struct CIMetaView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
var metaColor: Color
|
||||
var metaColor = Color.secondary
|
||||
var paleMetaColor = Color(UIColor.tertiaryLabel)
|
||||
var showStatus = true
|
||||
var showEdited = true
|
||||
@@ -64,7 +63,6 @@ func ciMetaText(
|
||||
chatTTL: Int?,
|
||||
encrypted: Bool?,
|
||||
color: Color = .clear,
|
||||
primaryColor: Color = .accentColor,
|
||||
transparent: Bool = false,
|
||||
sent: SentCheckmark? = nil,
|
||||
showStatus: Bool = true,
|
||||
@@ -87,7 +85,7 @@ func ciMetaText(
|
||||
r = r + statusIconText("arrow.forward", color.opacity(0.67)).font(.caption2)
|
||||
}
|
||||
if showStatus {
|
||||
if let (icon, statusColor) = meta.statusIcon(color, primaryColor) {
|
||||
if let (icon, statusColor) = meta.statusIcon(color) {
|
||||
let t = Text(Image(systemName: icon)).font(.caption2)
|
||||
let gap = Text(" ").kerning(-1.25)
|
||||
let t1 = t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67))
|
||||
@@ -114,16 +112,15 @@ private func statusIconText(_ icon: String, _ color: Color) -> Text {
|
||||
}
|
||||
|
||||
struct CIMetaView_Previews: PreviewProvider {
|
||||
static let metaColor = Color.secondary
|
||||
static var previews: some View {
|
||||
Group {
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete)), metaColor: metaColor)
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial)), metaColor: metaColor)
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete)), metaColor: metaColor)
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial)), metaColor: metaColor)
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete)), metaColor: metaColor)
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true), metaColor: metaColor)
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(), metaColor: metaColor)
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete)))
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial)))
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete)))
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial)))
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete)))
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true))
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample())
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 100))
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ let decryptErrorReason: LocalizedStringKey = "It can happen when you or your con
|
||||
|
||||
struct CIRcvDecryptionError: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
var msgDecryptError: MsgDecryptError
|
||||
var msgCount: UInt32
|
||||
@@ -115,23 +114,24 @@ struct CIRcvDecryptionError: View {
|
||||
}
|
||||
(
|
||||
Text(Image(systemName: "exclamationmark.arrow.triangle.2.circlepath"))
|
||||
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
|
||||
.foregroundColor(syncSupported ? .accentColor : .secondary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ Text("Fix connection")
|
||||
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
|
||||
.foregroundColor(syncSupported ? .accentColor : .secondary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.onTapGesture(perform: { onClick() })
|
||||
.padding(.vertical, 6)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
||||
@@ -145,12 +145,13 @@ struct CIRcvDecryptionError: View {
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.onTapGesture(perform: { onClick() })
|
||||
.padding(.vertical, 6)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import Combine
|
||||
|
||||
struct CIVideoView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
private let chatItem: ChatItem
|
||||
private let preview: UIImage?
|
||||
@State private var duration: Int
|
||||
|
||||
@@ -11,7 +11,6 @@ import SimpleXChat
|
||||
|
||||
struct CIVoiceView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
let recordingFile: CIFile?
|
||||
let duration: Int
|
||||
@@ -73,7 +72,7 @@ struct CIVoiceView: View {
|
||||
playbackState: $playbackState,
|
||||
playbackTime: $playbackTime
|
||||
)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
private func playbackSlider() -> some View {
|
||||
@@ -90,11 +89,10 @@ struct CIVoiceView: View {
|
||||
allowMenu = true
|
||||
}
|
||||
}
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
|
||||
private func metaView() -> some View {
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +118,7 @@ struct VoiceMessagePlayerTime: View {
|
||||
|
||||
struct VoiceMessagePlayer: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
var chatItem: ChatItem
|
||||
var recordingFile: CIFile?
|
||||
var recordingTime: TimeInterval
|
||||
@@ -218,26 +216,26 @@ struct VoiceMessagePlayer: View {
|
||||
startPlayback(recordingSource)
|
||||
}
|
||||
} label: {
|
||||
playPauseIcon("play.fill", theme.colors.primary)
|
||||
playPauseIcon("play.fill")
|
||||
}
|
||||
case .playing:
|
||||
Button {
|
||||
audioPlayer?.pause()
|
||||
playbackState = .paused
|
||||
} label: {
|
||||
playPauseIcon("pause.fill", theme.colors.primary)
|
||||
playPauseIcon("pause.fill")
|
||||
}
|
||||
case .paused:
|
||||
Button {
|
||||
audioPlayer?.play()
|
||||
playbackState = .playing
|
||||
} label: {
|
||||
playPauseIcon("play.fill", theme.colors.primary)
|
||||
playPauseIcon("play.fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func playPauseIcon(_ image: String, _ color: Color/* = .accentColor*/) -> some View {
|
||||
private func playPauseIcon(_ image: String, _ color: Color = .accentColor) -> some View {
|
||||
ZStack {
|
||||
Image(systemName: image)
|
||||
.resizable()
|
||||
@@ -246,7 +244,7 @@ struct VoiceMessagePlayer: View {
|
||||
.foregroundColor(color)
|
||||
.padding(.leading, image == "play.fill" ? 4 : 0)
|
||||
.frame(width: 56, height: 56)
|
||||
.background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear)
|
||||
.background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear)
|
||||
.clipShape(Circle())
|
||||
if recordingTime > 0 {
|
||||
ProgressCircle(length: recordingTime, progress: $playbackTime)
|
||||
@@ -263,12 +261,11 @@ struct VoiceMessagePlayer: View {
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
playPauseIcon(icon, theme.colors.primary)
|
||||
playPauseIcon(icon)
|
||||
}
|
||||
}
|
||||
|
||||
private struct ProgressCircle: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var length: TimeInterval
|
||||
@Binding var progress: TimeInterval?
|
||||
|
||||
@@ -276,7 +273,7 @@ struct VoiceMessagePlayer: View {
|
||||
Circle()
|
||||
.trim(from: 0, to: ((progress ?? TimeInterval(0)) / length))
|
||||
.stroke(
|
||||
theme.colors.primary,
|
||||
Color.accentColor,
|
||||
style: StrokeStyle(lineWidth: 3)
|
||||
)
|
||||
.rotationEffect(.degrees(-90))
|
||||
@@ -291,7 +288,7 @@ struct VoiceMessagePlayer: View {
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
||||
.frame(width: 56, height: 56)
|
||||
.background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear)
|
||||
.background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
@@ -299,7 +296,7 @@ struct VoiceMessagePlayer: View {
|
||||
ProgressView()
|
||||
.frame(width: 30, height: 30)
|
||||
.frame(width: 56, height: 56)
|
||||
.background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear)
|
||||
.background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
|
||||
@@ -10,21 +10,22 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct DeletedItemView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .bottom, spacing: 0) {
|
||||
Text(chatItem.content.text)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.italic()
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(chatItemFrameColor(chatItem, theme))
|
||||
.background(chatItemFrameColor(chatItem, colorScheme))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import SimpleXChat
|
||||
|
||||
struct EmojiItemView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
|
||||
var body: some View {
|
||||
@@ -19,7 +18,7 @@ struct EmojiItemView: View {
|
||||
emojiText(chatItem.content.text)
|
||||
.padding(.top, 8)
|
||||
.padding(.horizontal, 6)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
.padding(.bottom, 8)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct FramedCIVoiceView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
let recordingFile: CIFile?
|
||||
let duration: Int
|
||||
@@ -43,7 +42,7 @@ struct FramedCIVoiceView: View {
|
||||
playbackState: $playbackState,
|
||||
playbackTime: $playbackTime
|
||||
)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: 50, alignment: .leading)
|
||||
if .playing == playbackState || (playbackTime ?? 0) > 0 || !allowMenu {
|
||||
playbackSlider()
|
||||
|
||||
@@ -9,10 +9,17 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
let notesChatColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.21)
|
||||
let notesChatColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.19)
|
||||
let sentColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12)
|
||||
let sentColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.17)
|
||||
private let sentQuoteColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.11)
|
||||
private let sentQuoteColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.09)
|
||||
|
||||
struct FramedItemView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@EnvironmentObject var scrollModel: ReverseListScrollModel<ChatItem>
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var preview: UIImage?
|
||||
@@ -21,7 +28,7 @@ struct FramedItemView: View {
|
||||
@State var msgWidth: CGFloat = 0
|
||||
var imgWidth: CGFloat? = nil
|
||||
var videoWidth: CGFloat? = nil
|
||||
@State private var useWhiteMetaColor: Bool = false
|
||||
@State var metaColor = Color.secondary
|
||||
@State var showFullScreenImage = false
|
||||
@Binding var allowMenu: Bool
|
||||
@State private var showSecrets = false
|
||||
@@ -66,16 +73,19 @@ struct FramedItemView: View {
|
||||
.padding(chatItem.content.msgContent != nil ? 0 : 4)
|
||||
.overlay(DetermineWidth())
|
||||
}
|
||||
.onPreferenceChange(MetaColorPreferenceKey.self) { metaColor = $0 }
|
||||
|
||||
if chatItem.content.msgContent != nil {
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: useWhiteMetaColor ? Color.white : theme.colors.secondary)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: metaColor)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.bottom, 6)
|
||||
.overlay(DetermineWidth())
|
||||
.accessibilityLabel("")
|
||||
}
|
||||
}
|
||||
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, theme))
|
||||
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, colorScheme))
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(18)
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
|
||||
|
||||
if let (title, text) = chatItem.meta.itemStatus.statusInfo {
|
||||
@@ -111,12 +121,10 @@ struct FramedItemView: View {
|
||||
if text == "" && !chatItem.meta.isLive {
|
||||
Color.clear
|
||||
.frame(width: 0, height: 0)
|
||||
.onAppear {
|
||||
useWhiteMetaColor = true
|
||||
}
|
||||
.onDisappear {
|
||||
useWhiteMetaColor = false
|
||||
}
|
||||
.preference(
|
||||
key: MetaColorPreferenceKey.self,
|
||||
value: .white
|
||||
)
|
||||
} else {
|
||||
ciMsgContentView(chatItem)
|
||||
}
|
||||
@@ -126,12 +134,10 @@ struct FramedItemView: View {
|
||||
if text == "" && !chatItem.meta.isLive {
|
||||
Color.clear
|
||||
.frame(width: 0, height: 0)
|
||||
.onAppear {
|
||||
useWhiteMetaColor = true
|
||||
}
|
||||
.onDisappear {
|
||||
useWhiteMetaColor = false
|
||||
}
|
||||
.preference(
|
||||
key: MetaColorPreferenceKey.self,
|
||||
value: .white
|
||||
)
|
||||
} else {
|
||||
ciMsgContentView(chatItem)
|
||||
}
|
||||
@@ -170,13 +176,12 @@ struct FramedItemView: View {
|
||||
.font(.caption)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.top, 6)
|
||||
.padding(.bottom, pad || (chatItem.quotedItem == nil && chatItem.meta.itemForwarded == nil) ? 6 : 0)
|
||||
.overlay(DetermineWidth())
|
||||
.frame(minWidth: msgWidth, alignment: .leading)
|
||||
.background(chatItemFrameContextColor(chatItem, theme))
|
||||
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
|
||||
v.frame(maxWidth: mediaWidth, alignment: .leading)
|
||||
} else {
|
||||
@@ -228,7 +233,7 @@ struct FramedItemView: View {
|
||||
// if enable this always, size of the framed voice message item will be incorrect after end of playback
|
||||
.overlay { if case .voice = chatItem.content.msgContent {} else { DetermineWidth() } }
|
||||
.frame(minWidth: msgWidth, alignment: .leading)
|
||||
.background(chatItemFrameContextColor(chatItem, theme))
|
||||
.background(chatItemFrameContextColor(chatItem, colorScheme))
|
||||
|
||||
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
|
||||
v.frame(maxWidth: mediaWidth, alignment: .leading)
|
||||
@@ -243,7 +248,7 @@ struct FramedItemView: View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(sender)
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
ciQuotedMsgTextView(qi, lines: 2)
|
||||
}
|
||||
@@ -342,6 +347,13 @@ func isRightToLeft(_ s: String) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
private struct MetaColorPreferenceKey: PreferenceKey {
|
||||
static var defaultValue = Color.secondary
|
||||
static func reduce(value: inout Color, nextValue: () -> Color) {
|
||||
value = nextValue()
|
||||
}
|
||||
}
|
||||
|
||||
func onlyImageOrVideo(_ ci: ChatItem) -> Bool {
|
||||
if case let .image(text, _) = ci.content.msgContent {
|
||||
return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && ci.meta.itemForwarded == nil && text == ""
|
||||
@@ -351,22 +363,22 @@ func onlyImageOrVideo(_ ci: ChatItem) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func chatItemFrameColorMaybeImageOrVideo(_ ci: ChatItem, _ theme: AppTheme) -> Color {
|
||||
func chatItemFrameColorMaybeImageOrVideo(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
|
||||
onlyImageOrVideo(ci)
|
||||
? Color.clear
|
||||
: chatItemFrameColor(ci, theme)
|
||||
: chatItemFrameColor(ci, colorScheme)
|
||||
}
|
||||
|
||||
func chatItemFrameColor(_ ci: ChatItem, _ theme: AppTheme) -> Color {
|
||||
func chatItemFrameColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
|
||||
ci.chatDir.sent
|
||||
? theme.appColors.sentMessage
|
||||
: theme.appColors.receivedMessage
|
||||
? (colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
: Color(uiColor: .tertiarySystemGroupedBackground)
|
||||
}
|
||||
|
||||
func chatItemFrameContextColor(_ ci: ChatItem, _ theme: AppTheme) -> Color {
|
||||
func chatItemFrameContextColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
|
||||
ci.chatDir.sent
|
||||
? theme.appColors.sentQuote
|
||||
: theme.appColors.receivedQuote
|
||||
? (colorScheme == .light ? sentQuoteColorLight : sentQuoteColorDark)
|
||||
: Color(uiColor: .quaternarySystemFill)
|
||||
}
|
||||
|
||||
struct FramedItemView_Previews: PreviewProvider {
|
||||
|
||||
@@ -11,7 +11,6 @@ import SimpleXChat
|
||||
|
||||
struct IntegrityErrorItemView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var msgError: MsgErrorType
|
||||
var chatItem: ChatItem
|
||||
|
||||
@@ -55,7 +54,6 @@ struct IntegrityErrorItemView: View {
|
||||
|
||||
struct CIMsgError: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
var onTap: () -> Void
|
||||
|
||||
@@ -64,12 +62,13 @@ struct CIMsgError: View {
|
||||
Text(chatItem.content.text)
|
||||
.foregroundColor(.red)
|
||||
.italic()
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
.onTapGesture(perform: onTap)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct MarkedDeletedItemView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
@Binding var revealed: Bool
|
||||
@@ -19,10 +19,11 @@ struct MarkedDeletedItemView: View {
|
||||
var body: some View {
|
||||
(Text(mergedMarkedDeletedText).italic() + Text(" ") + chatItem.timestampText)
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(chatItemFrameColor(chatItem, theme))
|
||||
.background(chatItemFrameColor(chatItem, colorScheme))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ private func typing(_ w: Font.Weight = .light) -> Text {
|
||||
|
||||
struct MsgContentView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var text: String
|
||||
var formattedText: [FormattedText]? = nil
|
||||
var sender: String? = nil
|
||||
@@ -66,7 +65,7 @@ struct MsgContentView: View {
|
||||
}
|
||||
|
||||
private func msgContentView() -> Text {
|
||||
var v = messageText(text, formattedText, sender, showSecrets: showSecrets, secondaryColor: theme.colors.secondary)
|
||||
var v = messageText(text, formattedText, sender, showSecrets: showSecrets)
|
||||
if let mt = meta {
|
||||
if mt.isLive {
|
||||
v = v + typingIndicator(mt.recent)
|
||||
@@ -80,7 +79,7 @@ struct MsgContentView: View {
|
||||
return (recent ? typingIndicators[typingIdx] : noTyping)
|
||||
.font(.body.monospaced())
|
||||
.kerning(-2)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
|
||||
@@ -88,7 +87,7 @@ struct MsgContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, icon: String? = nil, preview: Bool = false, showSecrets: Bool, secondaryColor: Color) -> Text {
|
||||
func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, icon: String? = nil, preview: Bool = false, showSecrets: Bool) -> Text {
|
||||
let s = text
|
||||
var res: Text
|
||||
if let ft = formattedText, ft.count > 0 && ft.count <= 200 {
|
||||
@@ -103,7 +102,7 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: St
|
||||
}
|
||||
|
||||
if let i = icon {
|
||||
res = Text(Image(systemName: i)).foregroundColor(secondaryColor) + Text(" ") + res
|
||||
res = Text(Image(systemName: i)).foregroundColor(Color(uiColor: .tertiaryLabel)) + Text(" ") + res
|
||||
}
|
||||
|
||||
if let s = sender {
|
||||
|
||||
@@ -11,7 +11,6 @@ import SimpleXChat
|
||||
|
||||
struct ChatItemForwardingView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var ci: ChatItem
|
||||
@@ -39,7 +38,6 @@ struct ChatItemForwardingView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
.alert(item: $alert) { $0.alert }
|
||||
}
|
||||
|
||||
@@ -47,7 +45,7 @@ struct ChatItemForwardingView: View {
|
||||
VStack(alignment: .leading) {
|
||||
if !chatsToForwardTo.isEmpty {
|
||||
List {
|
||||
searchFieldView(text: $searchText, focussed: $searchFocused, theme.colors.onBackground, theme.colors.secondary)
|
||||
searchFieldView(text: $searchText, focussed: $searchFocused)
|
||||
.padding(.leading, 2)
|
||||
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
|
||||
let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { foundChat($0, s) }
|
||||
@@ -56,13 +54,8 @@ struct ChatItemForwardingView: View {
|
||||
.disabled(chatModel.deletedChats.contains(chat.chatInfo.id))
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} else {
|
||||
ZStack {
|
||||
emptyList()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.modifier(ThemedBackground())
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,10 +106,10 @@ struct ChatItemForwardingView: View {
|
||||
|
||||
private func emptyList() -> some View {
|
||||
Text("No filtered chats")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder private func forwardListChatView(_ chat: Chat) -> some View {
|
||||
let prohibited = prohibitedByPref(chat)
|
||||
Button {
|
||||
@@ -146,7 +139,7 @@ struct ChatItemForwardingView: View {
|
||||
ChatInfoImage(chat: chat, size: 30)
|
||||
.padding(.trailing, 2)
|
||||
Text(chat.chatInfo.chatViewName)
|
||||
.foregroundColor(prohibited ? theme.colors.secondary : theme.colors.onBackground)
|
||||
.foregroundColor(prohibited ? .secondary : .primary)
|
||||
.lineLimit(1)
|
||||
if chat.chatInfo.incognito {
|
||||
Spacer()
|
||||
@@ -154,7 +147,7 @@ struct ChatItemForwardingView: View {
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 22, height: 22)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -188,5 +181,5 @@ private func canForwardToChat(_ chat: Chat) -> Bool {
|
||||
ci: ChatItem.getSample(1, .directSnd, .now, "hello"),
|
||||
fromChatInfo: .direct(contact: Contact.sampleData),
|
||||
composeState: Binding.constant(ComposeState(message: "hello"))
|
||||
).environmentObject(CurrentColors.toAppTheme())
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import SimpleXChat
|
||||
struct ChatItemInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
var ci: ChatItem
|
||||
@Binding var chatItemInfo: ChatItemInfo?
|
||||
@State private var selection: CIInfoTab = .history
|
||||
@@ -101,14 +101,12 @@ struct ChatItemInfoView: View {
|
||||
Label("History", systemImage: "clock")
|
||||
}
|
||||
.tag(CIInfoTab.history)
|
||||
.modifier(ThemedBackground())
|
||||
if let qi = ci.quotedItem {
|
||||
quoteTab(qi)
|
||||
.tabItem {
|
||||
Label("In reply to", systemImage: "arrowshape.turn.up.left")
|
||||
}
|
||||
.tag(CIInfoTab.quote)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
if let forwardedFromItem = chatItemInfo?.forwardedFromChatItem {
|
||||
forwardedFromTab(forwardedFromItem)
|
||||
@@ -116,7 +114,6 @@ struct ChatItemInfoView: View {
|
||||
Label(local ? "Saved" : "Forwarded", systemImage: "arrowshape.turn.up.forward")
|
||||
}
|
||||
.tag(CIInfoTab.forwarded)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
@@ -126,7 +123,6 @@ struct ChatItemInfoView: View {
|
||||
}
|
||||
} else {
|
||||
historyTab()
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +212,7 @@ struct ChatItemInfoView: View {
|
||||
}
|
||||
else {
|
||||
Text("No history")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
@@ -231,8 +227,8 @@ struct ChatItemInfoView: View {
|
||||
textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(chatItemFrameColor(ci, theme))
|
||||
.modifier(ChatItemClipped())
|
||||
.background(chatItemFrameColor(ci, colorScheme))
|
||||
.cornerRadius(18)
|
||||
.contextMenu {
|
||||
if itemVersion.msgContent.text != "" {
|
||||
Button {
|
||||
@@ -262,19 +258,18 @@ struct ChatItemInfoView: View {
|
||||
} else {
|
||||
Text("no text")
|
||||
.italic()
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
private struct TextBubble: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var text: String
|
||||
var formattedText: [FormattedText]?
|
||||
var sender: String? = nil
|
||||
@State private var showSecrets = false
|
||||
|
||||
var body: some View {
|
||||
toggleSecrets(formattedText, $showSecrets, messageText(text, formattedText, sender, showSecrets: showSecrets, secondaryColor: theme.colors.secondary))
|
||||
toggleSecrets(formattedText, $showSecrets, messageText(text, formattedText, sender, showSecrets: showSecrets))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,8 +296,8 @@ struct ChatItemInfoView: View {
|
||||
textBubble(qi.text, qi.formattedText, qi.getSender(nil))
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(quotedMsgFrameColor(qi, theme))
|
||||
.modifier(ChatItemClipped())
|
||||
.background(quotedMsgFrameColor(qi, colorScheme))
|
||||
.cornerRadius(18)
|
||||
.contextMenu {
|
||||
if qi.text != "" {
|
||||
Button {
|
||||
@@ -325,10 +320,10 @@ struct ChatItemInfoView: View {
|
||||
.frame(maxWidth: maxWidth, alignment: .leading)
|
||||
}
|
||||
|
||||
func quotedMsgFrameColor(_ qi: CIQuote, _ theme: AppTheme) -> Color {
|
||||
func quotedMsgFrameColor(_ qi: CIQuote, _ colorScheme: ColorScheme) -> Color {
|
||||
(qi.chatDir?.sent ?? false)
|
||||
? theme.appColors.sentMessage
|
||||
: theme.appColors.receivedMessage
|
||||
? (colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
: Color(uiColor: .tertiarySystemGroupedBackground)
|
||||
}
|
||||
|
||||
@ViewBuilder private func forwardedFromTab(_ forwardedFromItem: AChatItem) -> some View {
|
||||
@@ -363,7 +358,7 @@ struct ChatItemInfoView: View {
|
||||
Divider().padding(.top, 32)
|
||||
Text("Recipient(s) can't see who this message is from.")
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,23 +372,23 @@ struct ChatItemInfoView: View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("you")
|
||||
.italic()
|
||||
.foregroundColor(theme.colors.onBackground)
|
||||
.foregroundColor(.primary)
|
||||
Text(forwardedFromItem.chatInfo.chatViewName)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
} else if case let .groupRcv(groupMember) = forwardedFromItem.chatItem.chatDir {
|
||||
VStack(alignment: .leading) {
|
||||
Text(groupMember.chatViewName)
|
||||
.foregroundColor(theme.colors.onBackground)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
Text(forwardedFromItem.chatInfo.chatViewName)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
} else {
|
||||
Text(forwardedFromItem.chatInfo.chatViewName)
|
||||
.foregroundColor(theme.colors.onBackground)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
@@ -415,7 +410,7 @@ struct ChatItemInfoView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private func memberDeliveryStatusesView(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View {
|
||||
LazyVStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
let mss = membersStatuses(memberDeliveryStatuses)
|
||||
if !mss.isEmpty {
|
||||
ForEach(mss, id: \.0.groupMemberId) { memberStatus in
|
||||
@@ -423,12 +418,12 @@ struct ChatItemInfoView: View {
|
||||
}
|
||||
} else {
|
||||
Text("No delivery information")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, GroupSndStatus, Bool?)] {
|
||||
private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus, Bool?)] {
|
||||
memberDeliveryStatuses.compactMap({ mds in
|
||||
if let mem = chatModel.getGroupMember(mds.groupMemberId) {
|
||||
return (mem.wrapped, mds.memberDeliveryStatus, mds.sentViaProxy)
|
||||
@@ -438,7 +433,7 @@ struct ChatItemInfoView: View {
|
||||
})
|
||||
}
|
||||
|
||||
private func memberDeliveryStatusView(_ member: GroupMember, _ status: GroupSndStatus, _ sentViaProxy: Bool?) -> some View {
|
||||
private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus, _ sentViaProxy: Bool?) -> some View {
|
||||
HStack{
|
||||
ProfileImage(imageStr: member.image, size: 30)
|
||||
.padding(.trailing, 2)
|
||||
@@ -447,22 +442,26 @@ struct ChatItemInfoView: View {
|
||||
Spacer()
|
||||
if sentViaProxy == true {
|
||||
Image(systemName: "arrow.forward")
|
||||
.foregroundColor(theme.colors.secondary).opacity(0.67)
|
||||
.foregroundColor(.secondary).opacity(0.67)
|
||||
}
|
||||
let v = Group {
|
||||
let (icon, statusColor) = status.statusIcon(theme.colors.secondary, theme.colors.primary)
|
||||
switch status {
|
||||
case .rcvd:
|
||||
ZStack(alignment: .trailing) {
|
||||
if let (icon, statusColor) = status.statusIcon(Color.secondary) {
|
||||
switch status {
|
||||
case .sndRcvd:
|
||||
ZStack(alignment: .trailing) {
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(statusColor.opacity(0.67))
|
||||
.padding(.trailing, 6)
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(statusColor.opacity(0.67))
|
||||
}
|
||||
default:
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(statusColor.opacity(0.67))
|
||||
.padding(.trailing, 6)
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(statusColor.opacity(0.67))
|
||||
.foregroundColor(statusColor)
|
||||
}
|
||||
default:
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(statusColor)
|
||||
} else {
|
||||
Image(systemName: "ellipsis")
|
||||
.foregroundColor(Color.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import SimpleXChat
|
||||
|
||||
struct ChatItemView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
var maxWidth: CGFloat = .infinity
|
||||
@Binding var revealed: Bool
|
||||
@@ -96,7 +95,6 @@ struct ChatItemView: View {
|
||||
|
||||
struct ChatItemContentView<Content: View>: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
@Binding var revealed: Bool
|
||||
@@ -126,14 +124,14 @@ struct ChatItemContentView<Content: View>: View {
|
||||
case .sndGroupEvent: eventItemView()
|
||||
case .rcvConnEvent: eventItemView()
|
||||
case .sndConnEvent: eventItemView()
|
||||
case let .rcvChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor(theme.colors.secondary))
|
||||
case let .sndChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor(theme.colors.secondary))
|
||||
case let .rcvChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor)
|
||||
case let .sndChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor)
|
||||
case let .rcvChatPreference(feature, allowed, param):
|
||||
CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param)
|
||||
case let .sndChatPreference(feature, _, _):
|
||||
CIChatFeatureView(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: theme.colors.secondary)
|
||||
case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary))
|
||||
case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary))
|
||||
CIChatFeatureView(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: .secondary)
|
||||
case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor)
|
||||
case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor)
|
||||
case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red)
|
||||
case let .rcvGroupFeatureRejected(feature): chatFeatureView(feature, .red)
|
||||
case .sndModerated: deletedItemView()
|
||||
@@ -160,20 +158,20 @@ struct ChatItemContentView<Content: View>: View {
|
||||
}
|
||||
|
||||
private func eventItemView() -> some View {
|
||||
CIEventView(eventText: eventItemViewText(theme.colors.secondary))
|
||||
return CIEventView(eventText: eventItemViewText())
|
||||
}
|
||||
|
||||
private func eventItemViewText(_ secondaryColor: Color) -> Text {
|
||||
private func eventItemViewText() -> Text {
|
||||
if !revealed, let t = mergedGroupEventText {
|
||||
return chatEventText(t + Text(" ") + chatItem.timestampText, secondaryColor)
|
||||
return chatEventText(t + Text(" ") + chatItem.timestampText)
|
||||
} else if let member = chatItem.memberDisplayName {
|
||||
return Text(member + " ")
|
||||
.font(.caption)
|
||||
.foregroundColor(secondaryColor)
|
||||
.foregroundColor(.secondary)
|
||||
.fontWeight(.light)
|
||||
+ chatEventText(chatItem, secondaryColor)
|
||||
+ chatEventText(chatItem)
|
||||
} else {
|
||||
return chatEventText(chatItem, secondaryColor)
|
||||
return chatEventText(chatItem)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +206,7 @@ struct ChatItemContentView<Content: View>: View {
|
||||
info.pqEnabled
|
||||
? Text("Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.")
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.fontWeight(.light)
|
||||
: e2eeInfoNoPQText()
|
||||
}
|
||||
@@ -216,24 +214,24 @@ struct ChatItemContentView<Content: View>: View {
|
||||
private func e2eeInfoNoPQText() -> Text {
|
||||
Text("Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.")
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.fontWeight(.light)
|
||||
}
|
||||
}
|
||||
|
||||
func chatEventText(_ text: Text, _ secondaryColor: Color) -> Text {
|
||||
func chatEventText(_ text: Text) -> Text {
|
||||
text
|
||||
.font(.caption)
|
||||
.foregroundColor(secondaryColor)
|
||||
.foregroundColor(.secondary)
|
||||
.fontWeight(.light)
|
||||
}
|
||||
|
||||
func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text, _ secondaryColor: Color) -> Text {
|
||||
chatEventText(Text(eventText) + Text(" ") + ts, secondaryColor)
|
||||
func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text) -> Text {
|
||||
chatEventText(Text(eventText) + Text(" ") + ts)
|
||||
}
|
||||
|
||||
func chatEventText(_ ci: ChatItem, _ secondaryColor: Color) -> Text {
|
||||
chatEventText("\(ci.content.text)", ci.timestampText, secondaryColor)
|
||||
func chatEventText(_ ci: ChatItem) -> Text {
|
||||
chatEventText("\(ci.content.text)", ci.timestampText)
|
||||
}
|
||||
|
||||
struct ChatItemView_Previews: PreviewProvider {
|
||||
|
||||
@@ -15,9 +15,8 @@ private let memberImageSize: CGFloat = 34
|
||||
|
||||
struct ChatView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State var theme: AppTheme = buildTheme()
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@State @ObservedObject var chat: Chat
|
||||
@@ -62,16 +61,7 @@ struct ChatView: View {
|
||||
Divider()
|
||||
}
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
let wallpaperImage = theme.wallpaper.type.image
|
||||
let wallpaperType = theme.wallpaper.type
|
||||
let backgroundColor = theme.wallpaper.background ?? wallpaperType.defaultBackgroundColor(theme.base, theme.colors.background)
|
||||
let tintColor = theme.wallpaper.tint ?? wallpaperType.defaultTintColor(theme.base)
|
||||
chatItemsList()
|
||||
.if(wallpaperImage != nil) { view in
|
||||
view.modifier(
|
||||
ChatViewBackground(image: wallpaperImage!, imageType: wallpaperType, background: backgroundColor, tint: tintColor)
|
||||
)
|
||||
}
|
||||
floatingButtons(counts: floatingButtonModel.unreadChatItemCounts)
|
||||
}
|
||||
connectingText()
|
||||
@@ -84,9 +74,7 @@ struct ChatView: View {
|
||||
}
|
||||
.padding(.top, 1)
|
||||
.navigationTitle(cInfo.chatViewName)
|
||||
.background(theme.colors.background)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.environmentObject(theme)
|
||||
.onAppear {
|
||||
loadChat(chat: chat)
|
||||
initChatView()
|
||||
@@ -98,7 +86,6 @@ struct ChatView: View {
|
||||
chat = c
|
||||
}
|
||||
initChatView()
|
||||
theme = buildTheme()
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
@@ -126,9 +113,6 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: colorScheme) { _ in
|
||||
theme = buildTheme()
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
if case let .direct(contact) = cInfo {
|
||||
@@ -157,7 +141,6 @@ struct ChatView: View {
|
||||
connectionStats = nil
|
||||
customUserProfile = nil
|
||||
connectionCode = nil
|
||||
theme = buildTheme()
|
||||
}) {
|
||||
ChatInfoView(chat: chat, contact: contact, connectionStats: $connectionStats, customUserProfile: $customUserProfile, localAlias: chat.chatInfo.localAlias, connectionCode: $connectionCode)
|
||||
}
|
||||
@@ -166,9 +149,8 @@ struct ChatView: View {
|
||||
Task { await loadGroupMembers(groupInfo) { showChatInfoSheet = true } }
|
||||
} label: {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
.appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) {
|
||||
.appSheet(isPresented: $showChatInfoSheet) {
|
||||
GroupChatInfoView(
|
||||
chat: chat,
|
||||
groupInfo: Binding(
|
||||
@@ -296,7 +278,7 @@ struct ChatView: View {
|
||||
Image(systemName: "magnifyingglass")
|
||||
TextField("Search", text: $searchText)
|
||||
.focused($searchFocussed)
|
||||
.foregroundColor(theme.colors.onBackground)
|
||||
.foregroundColor(.primary)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
Button {
|
||||
@@ -306,7 +288,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
.padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.background(Color(.tertiarySystemFill))
|
||||
.cornerRadius(10.0)
|
||||
|
||||
@@ -339,8 +321,8 @@ struct ChatView: View {
|
||||
}
|
||||
.map { $0.element }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private func chatItemsList() -> some View {
|
||||
let cInfo = chat.chatInfo
|
||||
let mergedItems = filtered(chatModel.reversedChatItems)
|
||||
@@ -389,7 +371,7 @@ struct ChatView: View {
|
||||
!contact.nextSendGrpInv {
|
||||
Text("connecting…")
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.top)
|
||||
} else {
|
||||
EmptyView()
|
||||
@@ -410,7 +392,7 @@ struct ChatView: View {
|
||||
|
||||
init() {
|
||||
unreadChatItemCounts = UnreadChatItemCounts(
|
||||
isNearBottom: true,
|
||||
totalBelow: .zero,
|
||||
unreadBelow: .zero
|
||||
)
|
||||
events
|
||||
@@ -425,9 +407,9 @@ struct ChatView: View {
|
||||
itemsInView
|
||||
}
|
||||
}
|
||||
.throttle(for: .seconds(0.2), scheduler: DispatchQueue.main, latest: true)
|
||||
.map { ChatModel.shared.unreadChatItemCounts(itemsInView: $0) }
|
||||
.removeDuplicates()
|
||||
.throttle(for: .seconds(0.2), scheduler: DispatchQueue.main, latest: true)
|
||||
.assign(to: \.unreadChatItemCounts, on: self)
|
||||
.store(in: &bag)
|
||||
}
|
||||
@@ -452,7 +434,7 @@ struct ChatView: View {
|
||||
circleButton {
|
||||
unreadCountText(unreadAbove)
|
||||
.font(.callout)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.onTapGesture {
|
||||
scrollModel.scrollToNextPage()
|
||||
@@ -472,17 +454,17 @@ struct ChatView: View {
|
||||
circleButton {
|
||||
unreadCountText(counts.unreadBelow)
|
||||
.font(.callout)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.onTapGesture {
|
||||
if let latestUnreadItem = filtered(chatModel.reversedChatItems).last(where: { $0.isRcvNew }) {
|
||||
scrollModel.scrollToItem(id: latestUnreadItem.id)
|
||||
}
|
||||
}
|
||||
} else if !counts.isNearBottom {
|
||||
} else if counts.totalBelow > 16 {
|
||||
circleButton {
|
||||
Image(systemName: "chevron.down")
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.onTapGesture { scrollModel.scrollToBottom() }
|
||||
}
|
||||
@@ -612,7 +594,7 @@ struct ChatView: View {
|
||||
|
||||
private struct ChatItemWithMenu: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var maxWidth: CGFloat
|
||||
@@ -629,6 +611,7 @@ struct ChatView: View {
|
||||
@State private var showChatItemInfoSheet: Bool = false
|
||||
@State private var chatItemInfo: ChatItemInfo?
|
||||
@State private var showForwardingSheet: Bool = false
|
||||
@State private var translateText: String?
|
||||
|
||||
@State private var allowMenu: Bool = true
|
||||
|
||||
@@ -656,7 +639,7 @@ struct ChatView: View {
|
||||
}
|
||||
.onAppear {
|
||||
markRead(
|
||||
chatItems: range.flatMap { m.reversedChatItems[$0] }
|
||||
chatItems: range.flatMap { m.reversedChatItems[$0] }
|
||||
?? [chatItem]
|
||||
)
|
||||
}
|
||||
@@ -696,7 +679,7 @@ struct ChatView: View {
|
||||
.padding(.top, 7)
|
||||
}
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
ProfileImage(imageStr: member.memberProfile.image, size: memberImageSize, backgroundColor: theme.colors.background)
|
||||
ProfileImage(imageStr: member.memberProfile.image, size: memberImageSize)
|
||||
.onTapGesture {
|
||||
if chatView.membersLoaded {
|
||||
selectedMember = m.getGroupMember(member.groupMemberId)
|
||||
@@ -754,7 +737,6 @@ struct ChatView: View {
|
||||
playbackState: $playbackState,
|
||||
playbackTime: $playbackTime
|
||||
)
|
||||
.modifier(ChatItemClipped(ci))
|
||||
.contextMenu { menu(ci, range, live: composeState.liveMessage != nil) }
|
||||
.accessibilityLabel("")
|
||||
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
|
||||
@@ -800,6 +782,7 @@ struct ChatView: View {
|
||||
ChatItemForwardingView(ci: ci, fromChatInfo: chat.chatInfo, composeState: $composeState)
|
||||
}
|
||||
}
|
||||
.translateSheet(text: $translateText)
|
||||
}
|
||||
|
||||
private func showMemberImage(_ member: GroupMember, _ prevItem: ChatItem?) -> Bool {
|
||||
@@ -822,7 +805,7 @@ struct ChatView: View {
|
||||
Text("\(r.totalReacted)")
|
||||
.font(.caption)
|
||||
.fontWeight(r.userReacted ? .bold : .light)
|
||||
.foregroundColor(r.userReacted ? theme.colors.primary : theme.colors.secondary)
|
||||
.foregroundColor(r.userReacted ? .accentColor : .secondary)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 6)
|
||||
@@ -856,6 +839,9 @@ struct ChatView: View {
|
||||
shareButton(ci)
|
||||
copyButton(ci)
|
||||
}
|
||||
if !mc.text.isEmpty {
|
||||
translateButton(text: mc.text)
|
||||
}
|
||||
if let fileSource = fileSource, fileExists {
|
||||
if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) {
|
||||
if image.imageData != nil {
|
||||
@@ -1036,6 +1022,17 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func translateButton(text: String) -> Button<some View> {
|
||||
Button {
|
||||
translateText = text
|
||||
} label: {
|
||||
Label(
|
||||
NSLocalizedString("Translate", comment: "chat item action"),
|
||||
systemImage: "character"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func saveButton(image: UIImage) -> Button<some View> {
|
||||
Button {
|
||||
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
|
||||
@@ -1325,27 +1322,6 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func buildTheme() -> AppTheme {
|
||||
if let cId = ChatModel.shared.chatId, let chat = ChatModel.shared.getChat(cId) {
|
||||
let perChatTheme = if case let .direct(contact) = chat.chatInfo {
|
||||
contact.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight)
|
||||
} else if case let .group(groupInfo) = chat.chatInfo {
|
||||
groupInfo.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight)
|
||||
} else {
|
||||
nil as ThemeModeOverride?
|
||||
}
|
||||
let overrides = if perChatTheme != nil {
|
||||
ThemeManager.currentColors(nil, perChatTheme, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
} else {
|
||||
nil as ThemeManager.ActiveTheme?
|
||||
}
|
||||
let theme = overrides ?? CurrentColors
|
||||
return AppTheme(name: theme.name, base: theme.base, colors: theme.colors, appColors: theme.appColors, wallpaper: theme.wallpaper)
|
||||
} else {
|
||||
return AppTheme.shared
|
||||
}
|
||||
}
|
||||
|
||||
struct ToggleNtfsButton: View {
|
||||
@ObservedObject var chat: Chat
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ComposeFileView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
let fileName: String
|
||||
let cancelFile: (() -> Void)
|
||||
let cancelEnabled: Bool
|
||||
@@ -33,7 +33,7 @@ struct ComposeFileView: View {
|
||||
.padding(.vertical, 1)
|
||||
.padding(.trailing, 12)
|
||||
.frame(height: 50)
|
||||
.background(theme.appColors.sentMessage)
|
||||
.background(colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ComposeImageView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
let images: [String]
|
||||
let cancelImage: (() -> Void)
|
||||
let cancelEnabled: Bool
|
||||
@@ -48,7 +48,7 @@ struct ComposeImageView: View {
|
||||
}
|
||||
.padding(.vertical, 1)
|
||||
.padding(.trailing, 12)
|
||||
.background(theme.appColors.sentMessage)
|
||||
.background(colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) {
|
||||
}
|
||||
|
||||
struct ComposeLinkView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
let linkPreview: LinkPreview?
|
||||
var cancelPreview: (() -> Void)? = nil
|
||||
let cancelEnabled: Bool
|
||||
@@ -62,7 +62,7 @@ struct ComposeLinkView: View {
|
||||
}
|
||||
.padding(.vertical, 1)
|
||||
.padding(.trailing, 12)
|
||||
.background(theme.appColors.sentMessage)
|
||||
.background(colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
@@ -82,7 +82,7 @@ struct ComposeLinkView: View {
|
||||
Text(linkPreview.uri.absoluteString)
|
||||
.font(.caption)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 5)
|
||||
.frame(maxWidth: .infinity, minHeight: 60, maxHeight: 60)
|
||||
|
||||
@@ -254,7 +254,6 @@ enum UploadContent: Equatable {
|
||||
|
||||
struct ComposeView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
@Binding var composeState: ComposeState
|
||||
@Binding var keyboardVisible: Bool
|
||||
@@ -315,7 +314,6 @@ struct ComposeView: View {
|
||||
.frame(width: 25, height: 25)
|
||||
.padding(.bottom, 12)
|
||||
.padding(.leading, 12)
|
||||
.tint(theme.colors.primary)
|
||||
if case let .group(g) = chat.chatInfo,
|
||||
!g.fullGroupPreferences.files.on(for: g.membership) {
|
||||
b.disabled(true).onTapGesture {
|
||||
@@ -356,16 +354,16 @@ struct ComposeView: View {
|
||||
keyboardVisible: $keyboardVisible,
|
||||
sendButtonColor: chat.chatInfo.incognito
|
||||
? .indigo.opacity(colorScheme == .dark ? 1 : 0.7)
|
||||
: theme.colors.primary
|
||||
: .accentColor
|
||||
)
|
||||
.padding(.trailing, 12)
|
||||
.background(theme.colors.background)
|
||||
.background(.background)
|
||||
.disabled(!chat.userCanSend)
|
||||
|
||||
if chat.userIsObserver {
|
||||
Text("you are observer")
|
||||
.italic()
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -657,7 +655,7 @@ struct ComposeView: View {
|
||||
|
||||
private func msgNotAllowedView(_ reason: LocalizedStringKey, icon: String) -> some View {
|
||||
HStack {
|
||||
Image(systemName: icon).foregroundColor(theme.colors.secondary)
|
||||
Image(systemName: icon).foregroundColor(.secondary)
|
||||
Text(reason).italic()
|
||||
}
|
||||
.padding(12)
|
||||
|
||||
@@ -25,7 +25,7 @@ func voiceMessageTime_(_ time: TimeInterval?) -> String {
|
||||
|
||||
struct ComposeVoiceView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
var recordingFileName: String
|
||||
@Binding var recordingTime: TimeInterval?
|
||||
@Binding var recordingState: VoiceMessageRecordingState
|
||||
@@ -50,7 +50,7 @@ struct ComposeVoiceView: View {
|
||||
}
|
||||
.padding(.vertical, 1)
|
||||
.frame(height: ComposeVoiceView.previewHeight)
|
||||
.background(theme.appColors.sentMessage)
|
||||
.background(colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
@@ -80,7 +80,7 @@ struct ComposeVoiceView: View {
|
||||
Button {
|
||||
startPlayback()
|
||||
} label: {
|
||||
playPauseIcon("play.fill", theme.colors.primary)
|
||||
playPauseIcon("play.fill")
|
||||
}
|
||||
Text(voiceMessageTime_(recordingTime))
|
||||
case .playing:
|
||||
@@ -88,7 +88,7 @@ struct ComposeVoiceView: View {
|
||||
audioPlayer?.pause()
|
||||
playbackState = .paused
|
||||
} label: {
|
||||
playPauseIcon("pause.fill", theme.colors.primary)
|
||||
playPauseIcon("pause.fill")
|
||||
}
|
||||
Text(voiceMessageTime_(playbackTime))
|
||||
case .paused:
|
||||
@@ -96,7 +96,7 @@ struct ComposeVoiceView: View {
|
||||
audioPlayer?.play()
|
||||
playbackState = .playing
|
||||
} label: {
|
||||
playPauseIcon("play.fill", theme.colors.primary)
|
||||
playPauseIcon("play.fill")
|
||||
}
|
||||
Text(voiceMessageTime_(playbackTime))
|
||||
}
|
||||
@@ -131,7 +131,7 @@ struct ComposeVoiceView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func playPauseIcon(_ image: String, _ color: Color) -> some View {
|
||||
private func playPauseIcon(_ image: String, _ color: Color = .accentColor) -> some View {
|
||||
Image(systemName: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
@@ -147,11 +147,9 @@ struct ComposeVoiceView: View {
|
||||
} label: {
|
||||
Image(systemName: "multiply")
|
||||
}
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
|
||||
struct SliderBar: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var length: TimeInterval
|
||||
@Binding var progress: TimeInterval?
|
||||
var seek: (TimeInterval) -> Void
|
||||
@@ -160,12 +158,10 @@ struct ComposeVoiceView: View {
|
||||
Slider(value: Binding(get: { progress ?? TimeInterval(0) }, set: { seek($0) }), in: 0 ... length)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 4)
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
private struct ProgressBar: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var length: TimeInterval
|
||||
@Binding var progress: TimeInterval?
|
||||
|
||||
@@ -173,7 +169,7 @@ struct ComposeVoiceView: View {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.fill(theme.colors.primary)
|
||||
.fill(Color.accentColor)
|
||||
.frame(width: min(CGFloat((progress ?? TimeInterval(0)) / length) * geometry.size.width, geometry.size.width), height: 4)
|
||||
.animation(.linear, value: progress)
|
||||
}
|
||||
|
||||
@@ -9,18 +9,18 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ContextInvitingContactMemberView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Image(systemName: "message")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
Text("Send direct message to connect")
|
||||
}
|
||||
.padding(12)
|
||||
.frame(minHeight: 50)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(theme.appColors.sentMessage)
|
||||
.background(colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ContextItemView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ObservedObject var chat: Chat
|
||||
let contextItem: ChatItem
|
||||
let contextIcon: String
|
||||
@@ -23,10 +23,10 @@ struct ContextItemView: View {
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 16, height: 16)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
if showSender, let sender = contextItem.memberDisplayName {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(sender).font(.caption).foregroundColor(theme.colors.secondary)
|
||||
Text(sender).font(.caption).foregroundColor(.secondary)
|
||||
msgContentView(lines: 2)
|
||||
}
|
||||
} else {
|
||||
@@ -40,12 +40,11 @@ struct ContextItemView: View {
|
||||
} label: {
|
||||
Image(systemName: "multiply")
|
||||
}
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
.padding(12)
|
||||
.frame(minHeight: 50)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(chatItemFrameColor(contextItem, theme))
|
||||
.background(chatItemFrameColor(contextItem, colorScheme))
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
@@ -56,7 +55,7 @@ struct ContextItemView: View {
|
||||
}
|
||||
|
||||
private func contextMsgPreview() -> Text {
|
||||
return attachment() + messageText(contextItem.text, contextItem.formattedText, nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary)
|
||||
return attachment() + messageText(contextItem.text, contextItem.formattedText, nil, preview: true, showSecrets: false)
|
||||
|
||||
func attachment() -> Text {
|
||||
switch contextItem.content.msgContent {
|
||||
|
||||
@@ -28,7 +28,6 @@ struct NativeTextEditor: UIViewRepresentable {
|
||||
|
||||
func makeUIView(context: Context) -> UITextView {
|
||||
let field = CustomUITextField(height: _height)
|
||||
field.backgroundColor = .clear
|
||||
field.text = text
|
||||
field.textAlignment = alignment(text)
|
||||
field.autocapitalizationType = .sentences
|
||||
|
||||
@@ -13,7 +13,6 @@ private let liveMsgInterval: UInt64 = 3000_000000
|
||||
|
||||
struct SendMessageView: View {
|
||||
@Binding var composeState: ComposeState
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var sendMessage: (Int?) -> Void
|
||||
var sendLiveMessage: (() async -> Void)? = nil
|
||||
var updateLiveMessage: (() async -> Void)? = nil
|
||||
@@ -50,7 +49,7 @@ struct SendMessageView: View {
|
||||
Text("Voice message…")
|
||||
.font(teFont.italic())
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 8)
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -248,7 +247,6 @@ struct SendMessageView: View {
|
||||
}
|
||||
|
||||
private struct RecordVoiceMessageButton: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var startVoiceMessageRecording: (() -> Void)?
|
||||
var finishVoiceMessageRecording: (() -> Void)?
|
||||
@Binding var holdingVMR: Bool
|
||||
@@ -258,7 +256,7 @@ struct SendMessageView: View {
|
||||
var body: some View {
|
||||
Button(action: {}) {
|
||||
Image(systemName: "mic.fill")
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.disabled(disabled)
|
||||
.frame(width: 29, height: 29)
|
||||
@@ -311,7 +309,7 @@ struct SendMessageView: View {
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "mic")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.disabled(composeState.inProgress)
|
||||
.frame(width: 29, height: 29)
|
||||
@@ -325,7 +323,7 @@ struct SendMessageView: View {
|
||||
Image(systemName: "multiply")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.foregroundColor(.accentColor)
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
.frame(width: 29, height: 29)
|
||||
@@ -342,7 +340,7 @@ struct SendMessageView: View {
|
||||
Image(systemName: "bolt.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.foregroundColor(.accentColor)
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
.frame(width: 29, height: 29)
|
||||
@@ -385,7 +383,7 @@ struct SendMessageView: View {
|
||||
}
|
||||
Task {
|
||||
_ = try? await Task.sleep(nanoseconds: liveMsgInterval)
|
||||
while await composeState.liveMessage != nil {
|
||||
while composeState.liveMessage != nil {
|
||||
await update()
|
||||
_ = try? await Task.sleep(nanoseconds: liveMsgInterval)
|
||||
}
|
||||
@@ -396,7 +394,7 @@ struct SendMessageView: View {
|
||||
private func finishVoiceMessageRecordingButton() -> some View {
|
||||
Button(action: { finishVoiceMessageRecording?() }) {
|
||||
Image(systemName: "stop.fill")
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.disabled(composeState.inProgress)
|
||||
.frame(width: 29, height: 29)
|
||||
|
||||
@@ -12,7 +12,6 @@ import SimpleXChat
|
||||
struct ContactPreferencesView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var contact: Contact
|
||||
@State var featuresAllowed: ContactFeaturesAllowed
|
||||
@State var currentFeaturesAllowed: ContactFeaturesAllowed
|
||||
@@ -67,8 +66,8 @@ struct ContactPreferencesView: View {
|
||||
.frame(height: 36)
|
||||
infoRow("Contact allows", pref.contactPreference.allow.text)
|
||||
}
|
||||
header: { featureHeader(feature, enabled).foregroundColor(theme.colors.secondary) }
|
||||
footer: { featureFooter(feature, enabled).foregroundColor(theme.colors.secondary) }
|
||||
header: { featureHeader(feature, enabled) }
|
||||
footer: { featureFooter(feature, enabled) }
|
||||
}
|
||||
|
||||
private func timedMessagesFeatureSection() -> some View {
|
||||
@@ -103,8 +102,8 @@ struct ContactPreferencesView: View {
|
||||
infoRow("Delete after", timeText(pref.contactPreference.ttl))
|
||||
}
|
||||
}
|
||||
header: { featureHeader(.timedMessages, enabled).foregroundColor(theme.colors.secondary) }
|
||||
footer: { featureFooter(.timedMessages, enabled).foregroundColor(theme.colors.secondary) }
|
||||
header: { featureHeader(.timedMessages, enabled) }
|
||||
footer: { featureFooter(.timedMessages, enabled) }
|
||||
}
|
||||
|
||||
private func featureHeader(_ feature: ChatFeature, _ enabled: FeatureEnabled) -> some View {
|
||||
|
||||
@@ -21,7 +21,6 @@ struct AddGroupMembersView: View {
|
||||
|
||||
struct AddGroupMembersViewCommon: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chat: Chat
|
||||
@State var groupInfo: GroupInfo
|
||||
var creatingGroup: Bool = false
|
||||
@@ -71,7 +70,7 @@ struct AddGroupMembersViewCommon: View {
|
||||
|
||||
if (membersToAdd.isEmpty) {
|
||||
Text("No contacts to add")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.listRowBackground(Color.clear)
|
||||
@@ -91,18 +90,16 @@ struct AddGroupMembersViewCommon: View {
|
||||
Button { selectedContacts.removeAll() } label: { Text("Clear").font(.caption) }
|
||||
Spacer()
|
||||
Text("\(count) contact(s) selected")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
} else {
|
||||
Text("No contacts selected")
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
searchFieldView(text: $searchText, focussed: $searchFocussed, theme.colors.primary, theme.colors.secondary)
|
||||
searchFieldView(text: $searchText, focussed: $searchFocussed)
|
||||
.padding(.leading, 2)
|
||||
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
|
||||
let members = s == "" ? membersToAdd : membersToAdd.filter { $0.chatViewName.localizedLowercase.contains(s) }
|
||||
@@ -128,7 +125,6 @@ struct AddGroupMembersViewCommon: View {
|
||||
.onChange(of: selectedContacts) { _ in
|
||||
searchFocussed = false
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
|
||||
private func inviteMembersButton() -> some View {
|
||||
@@ -176,14 +172,14 @@ struct AddGroupMembersViewCommon: View {
|
||||
var iconColor: Color
|
||||
if prohibitedToInviteIncognito {
|
||||
icon = "theatermasks.circle.fill"
|
||||
iconColor = Color(uiColor: .tertiaryLabel).asAnotherColorFromSecondary(theme)
|
||||
iconColor = Color(uiColor: .tertiaryLabel)
|
||||
} else {
|
||||
if checked {
|
||||
icon = "checkmark.circle.fill"
|
||||
iconColor = theme.colors.primary
|
||||
iconColor = .accentColor
|
||||
} else {
|
||||
icon = "circle"
|
||||
iconColor = Color(uiColor: .tertiaryLabel).asAnotherColorFromSecondary(theme)
|
||||
iconColor = Color(uiColor: .tertiaryLabel)
|
||||
}
|
||||
}
|
||||
return Button {
|
||||
@@ -201,7 +197,7 @@ struct AddGroupMembersViewCommon: View {
|
||||
ProfileImage(imageStr: contact.image, size: 30)
|
||||
.padding(.trailing, 2)
|
||||
Text(ChatInfo.direct(contact: contact).chatViewName)
|
||||
.foregroundColor(prohibitedToInviteIncognito ? theme.colors.secondary : theme.colors.onBackground)
|
||||
.foregroundColor(prohibitedToInviteIncognito ? .secondary : .primary)
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
Image(systemName: icon)
|
||||
@@ -211,7 +207,7 @@ struct AddGroupMembersViewCommon: View {
|
||||
}
|
||||
}
|
||||
|
||||
func searchFieldView(text: Binding<String>, focussed: FocusState<Bool>.Binding, _ onBackgroundColor: Color, _ secondaryColor: Color) -> some View {
|
||||
func searchFieldView(text: Binding<String>, focussed: FocusState<Bool>.Binding) -> some View {
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.resizable()
|
||||
@@ -220,7 +216,7 @@ func searchFieldView(text: Binding<String>, focussed: FocusState<Bool>.Binding,
|
||||
.padding(.trailing, 10)
|
||||
TextField("Search", text: text)
|
||||
.focused(focussed)
|
||||
.foregroundColor(onBackgroundColor)
|
||||
.foregroundColor(.primary)
|
||||
.frame(maxWidth: .infinity)
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.resizable()
|
||||
@@ -232,7 +228,7 @@ func searchFieldView(text: Binding<String>, focussed: FocusState<Bool>.Binding,
|
||||
focussed.wrappedValue = false
|
||||
}
|
||||
}
|
||||
.foregroundColor(secondaryColor)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(height: 36)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ let SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
|
||||
|
||||
struct GroupChatInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@ObservedObject var chat: Chat
|
||||
@Binding var groupInfo: GroupInfo
|
||||
@@ -82,19 +81,13 @@ struct GroupChatInfoView: View {
|
||||
} else {
|
||||
sendReceiptsOptionDisabled()
|
||||
}
|
||||
NavigationLink {
|
||||
ChatWallpaperEditorSheet(chat: chat)
|
||||
} label: {
|
||||
Label("Chat theme", systemImage: "photo")
|
||||
}
|
||||
} header: {
|
||||
Text("")
|
||||
} footer: {
|
||||
Text("Only group owners can change group preferences.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
|
||||
Section(header: Text("\(members.count + 1) members").foregroundColor(theme.colors.secondary)) {
|
||||
Section("\(members.count + 1) members") {
|
||||
if groupInfo.canAddMembers {
|
||||
groupLinkButton()
|
||||
if (chat.chatInfo.incognito) {
|
||||
@@ -106,7 +99,7 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
if members.count > 8 {
|
||||
searchFieldView(text: $searchText, focussed: $searchFocussed, theme.colors.onBackground, theme.colors.secondary)
|
||||
searchFieldView(text: $searchText, focussed: $searchFocussed)
|
||||
.padding(.leading, 8)
|
||||
}
|
||||
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
|
||||
@@ -136,13 +129,12 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
|
||||
if developerTools {
|
||||
Section(header: Text("For console").foregroundColor(theme.colors.secondary)) {
|
||||
Section(header: Text("For console")) {
|
||||
infoRow("Local name", chat.chatInfo.localDisplayName)
|
||||
infoRow("Database ID", "\(chat.chatInfo.apiId)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
@@ -218,7 +210,6 @@ struct GroupChatInfoView: View {
|
||||
private struct MemberRowView: View {
|
||||
var groupInfo: GroupInfo
|
||||
@ObservedObject var groupMember: GMember
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var user: Bool = false
|
||||
@Binding var alert: GroupChatInfoViewAlert?
|
||||
|
||||
@@ -229,13 +220,14 @@ struct GroupChatInfoView: View {
|
||||
.padding(.trailing, 2)
|
||||
// TODO server connection status
|
||||
VStack(alignment: .leading) {
|
||||
let t = Text(member.chatViewName).foregroundColor(member.memberIncognito ? .indigo : theme.colors.onBackground)
|
||||
let t = Text(member.chatViewName).foregroundColor(member.memberIncognito ? .indigo : .primary)
|
||||
(member.verified ? memberVerifiedShield + t : t)
|
||||
.lineLimit(1)
|
||||
(user ? Text ("you: ") + Text(member.memberStatus.shortText) : Text(memberConnStatus(member)))
|
||||
let s = Text(member.memberStatus.shortText)
|
||||
(user ? Text ("you: ") + s : s)
|
||||
.lineLimit(1)
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
memberInfo(member)
|
||||
@@ -265,25 +257,15 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func memberConnStatus(_ member: GroupMember) -> LocalizedStringKey {
|
||||
if member.activeConn?.connDisabled ?? false {
|
||||
return "disabled"
|
||||
} else if member.activeConn?.connInactive ?? false {
|
||||
return "inactive"
|
||||
} else {
|
||||
return member.memberStatus.shortText
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func memberInfo(_ member: GroupMember) -> some View {
|
||||
if member.blocked {
|
||||
Text("blocked")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
let role = member.memberRole
|
||||
if [.owner, .admin, .observer].contains(role) {
|
||||
Text(member.memberRole.text)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,13 +276,13 @@ struct GroupChatInfoView: View {
|
||||
Button {
|
||||
alert = .blockMemberAlert(mem: member)
|
||||
} label: {
|
||||
Label("Block member", systemImage: "hand.raised").foregroundColor(theme.colors.secondary)
|
||||
Label("Block member", systemImage: "hand.raised").foregroundColor(.secondary)
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
alert = .unblockMemberAlert(mem: member)
|
||||
} label: {
|
||||
Label("Unblock member", systemImage: "hand.raised.slash").foregroundColor(theme.colors.primary)
|
||||
Label("Unblock member", systemImage: "hand.raised.slash").foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -312,13 +294,13 @@ struct GroupChatInfoView: View {
|
||||
Button {
|
||||
alert = .unblockForAllAlert(mem: member)
|
||||
} label: {
|
||||
Label("Unblock for all", systemImage: "hand.raised.slash").foregroundColor(theme.colors.primary)
|
||||
Label("Unblock for all", systemImage: "hand.raised.slash").foregroundColor(.accentColor)
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
alert = .blockForAllAlert(mem: member)
|
||||
} label: {
|
||||
Label("Block for all", systemImage: "hand.raised").foregroundColor(theme.colors.secondary)
|
||||
Label("Block for all", systemImage: "hand.raised").foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,14 +316,6 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var memberVerifiedShield: Text {
|
||||
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
|
||||
.font(.caption)
|
||||
.baselineOffset(2)
|
||||
.kerning(-2)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
private func memberInfoView(_ groupMember: GMember) -> some View {
|
||||
@@ -359,7 +333,6 @@ struct GroupChatInfoView: View {
|
||||
creatingGroup: false
|
||||
)
|
||||
.navigationBarTitle("Group link")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
if groupLink == nil {
|
||||
@@ -377,7 +350,6 @@ struct GroupChatInfoView: View {
|
||||
groupProfile: groupInfo.groupProfile
|
||||
)
|
||||
.navigationBarTitle("Group profile")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
Label("Edit group profile", systemImage: "pencil")
|
||||
@@ -392,7 +364,6 @@ struct GroupChatInfoView: View {
|
||||
welcomeText: groupInfo.groupProfile.description ?? ""
|
||||
)
|
||||
.navigationTitle("Welcome message")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
groupInfo.groupProfile.description == nil
|
||||
@@ -547,7 +518,6 @@ func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bo
|
||||
creatingGroup: creatingGroup
|
||||
)
|
||||
.navigationBarTitle("Group preferences")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
if creatingGroup {
|
||||
@@ -558,6 +528,14 @@ func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bo
|
||||
}
|
||||
}
|
||||
|
||||
private var memberVerifiedShield: Text {
|
||||
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
|
||||
.font(.caption)
|
||||
.baselineOffset(2)
|
||||
.kerning(-2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
func cantInviteIncognitoAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text("Can't invite contacts!"),
|
||||
|
||||
@@ -133,7 +133,6 @@ struct GroupLinkView: View {
|
||||
shouldCreate = false
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
|
||||
private func createGroupLink() {
|
||||
|
||||
@@ -11,7 +11,6 @@ import SimpleXChat
|
||||
|
||||
struct GroupMemberInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@State var groupInfo: GroupInfo
|
||||
@ObservedObject var groupMember: GMember
|
||||
@@ -121,14 +120,12 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
} header: {
|
||||
Text("Address")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Member").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Member") {
|
||||
infoRow("Group", groupInfo.displayName)
|
||||
|
||||
if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
|
||||
@@ -141,10 +138,16 @@ struct GroupMemberInfoView: View {
|
||||
} else {
|
||||
infoRow("Role", member.memberRole.text)
|
||||
}
|
||||
|
||||
// TODO invited by - need to get contact by contact id
|
||||
if let conn = member.activeConn {
|
||||
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
|
||||
infoRow("Connection", connLevelDesc)
|
||||
}
|
||||
}
|
||||
|
||||
if let connStats = connectionStats {
|
||||
Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Servers") {
|
||||
// TODO network connection status
|
||||
Button("Change receiving address") {
|
||||
alert = .switchAddressAlert
|
||||
@@ -162,8 +165,8 @@ struct GroupMemberInfoView: View {
|
||||
|| connStats.ratchetSyncSendProhibited
|
||||
)
|
||||
}
|
||||
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }, theme.colors.secondary)
|
||||
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }, theme.colors.secondary)
|
||||
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer })
|
||||
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,13 +177,9 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
|
||||
if developerTools {
|
||||
Section(header: Text("For console").foregroundColor(theme.colors.secondary)) {
|
||||
Section("For console") {
|
||||
infoRow("Local name", member.localDisplayName)
|
||||
infoRow("Database ID", "\(member.groupMemberId)")
|
||||
if let conn = member.activeConn {
|
||||
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
|
||||
infoRow("Connection", connLevelDesc)
|
||||
}
|
||||
Button ("Debug delivery") {
|
||||
Task {
|
||||
do {
|
||||
@@ -248,7 +247,6 @@ struct GroupMemberInfoView: View {
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
|
||||
func connectViaAddressButton(_ contactLink: String) -> some View {
|
||||
@@ -328,7 +326,7 @@ struct GroupMemberInfoView: View {
|
||||
if mem.verified {
|
||||
(
|
||||
Text(Image(systemName: "checkmark.shield"))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.title2)
|
||||
+ Text(" ")
|
||||
+ Text(mem.displayName)
|
||||
@@ -376,7 +374,6 @@ struct GroupMemberInfoView: View {
|
||||
)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("Security code")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Label(
|
||||
member.verified ? "View security code" : "Verify security code",
|
||||
@@ -426,7 +423,7 @@ struct GroupMemberInfoView: View {
|
||||
Section {
|
||||
if mem.blockedByAdmin {
|
||||
Label("Blocked by admin", systemImage: "hand.raised")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
} else if mem.memberSettings.showMessages {
|
||||
blockMemberButton(mem)
|
||||
} else {
|
||||
|
||||
@@ -18,7 +18,6 @@ private let featureRoles: [(role: GroupMemberRole?, text: LocalizedStringKey)] =
|
||||
struct GroupPreferencesView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var groupInfo: GroupInfo
|
||||
@State var preferences: FullGroupPreferences
|
||||
@State var currentPreferences: FullGroupPreferences
|
||||
@@ -74,7 +73,7 @@ struct GroupPreferencesView: View {
|
||||
|
||||
private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding<GroupFeatureEnabled>, _ enableForRole: Binding<GroupMemberRole?>? = nil) -> some View {
|
||||
Section {
|
||||
let color: Color = enableFeature.wrappedValue == .on ? .green : theme.colors.secondary
|
||||
let color: Color = enableFeature.wrappedValue == .on ? .green : .secondary
|
||||
let icon = enableFeature.wrappedValue == .on ? feature.iconFilled : feature.icon
|
||||
let timedOn = feature == .timedMessages && enableFeature.wrappedValue == .on
|
||||
if groupInfo.canEdit {
|
||||
@@ -112,19 +111,18 @@ struct GroupPreferencesView: View {
|
||||
}
|
||||
if enableFeature.wrappedValue == .on, let enableForRole {
|
||||
HStack {
|
||||
Text("Enabled for").foregroundColor(theme.colors.secondary)
|
||||
Text("Enabled for").foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text(
|
||||
featureRoles.first(where: { fr in fr.role == enableForRole.wrappedValue })?.text
|
||||
?? "all members"
|
||||
)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
} footer: {
|
||||
Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.canEdit))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
.onChange(of: enableFeature.wrappedValue) { enabled in
|
||||
if case .off = enabled {
|
||||
|
||||
@@ -11,7 +11,6 @@ import SimpleXChat
|
||||
|
||||
struct GroupWelcomeView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var groupInfo: GroupInfo
|
||||
@State var groupProfile: GroupProfile
|
||||
@State var welcomeText: String
|
||||
@@ -58,7 +57,7 @@ struct GroupWelcomeView: View {
|
||||
}
|
||||
|
||||
private func textPreview() -> some View {
|
||||
messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false, secondaryColor: theme.colors.secondary)
|
||||
messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false)
|
||||
.frame(minHeight: 130, alignment: .topLeading)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
@@ -71,7 +70,7 @@ struct GroupWelcomeView: View {
|
||||
Group {
|
||||
if welcomeText.isEmpty {
|
||||
TextEditor(text: Binding.constant(NSLocalizedString("Enter welcome message…", comment: "placeholder")))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.disabled(true)
|
||||
}
|
||||
TextEditor(text: $welcomeText)
|
||||
|
||||
@@ -55,7 +55,6 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
|
||||
// 1. Style
|
||||
tableView.separatorStyle = .none
|
||||
tableView.transform = .verticalFlip
|
||||
tableView.backgroundColor = .clear
|
||||
|
||||
// 2. Register cells
|
||||
if #available(iOS 16.0, *) {
|
||||
@@ -91,7 +90,6 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
|
||||
}
|
||||
cell.transform = .verticalFlip
|
||||
cell.selectionStyle = .none
|
||||
cell.backgroundColor = .clear
|
||||
return cell
|
||||
}
|
||||
|
||||
@@ -181,7 +179,6 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
|
||||
/// Updates content of the cell
|
||||
/// For reference: https://noahgilmore.com/blog/swiftui-self-sizing-cells/
|
||||
func set(content: Hosted, parent: UIViewController) {
|
||||
hostingController.view.backgroundColor = .clear
|
||||
hostingController.rootView = content
|
||||
if let hostingView = hostingController.view {
|
||||
hostingView.invalidateIntrinsicContentSize()
|
||||
|
||||
@@ -10,7 +10,6 @@ import SwiftUI
|
||||
|
||||
struct VerifyCodeView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var displayName: String
|
||||
@State var connectionCode: String?
|
||||
@State var connectionVerified: Bool
|
||||
@@ -31,7 +30,7 @@ struct VerifyCodeView: View {
|
||||
HStack {
|
||||
if connectionVerified {
|
||||
Image(systemName: "checkmark.shield")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
Text("\(displayName) is verified")
|
||||
} else {
|
||||
Text("\(displayName) is not verified")
|
||||
@@ -67,7 +66,6 @@ struct VerifyCodeView: View {
|
||||
ScanCodeView(connectionVerified: $connectionVerified, verify: verify)
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.navigationTitle("Scan code")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Label("Scan code", systemImage: "qrcode")
|
||||
}
|
||||
@@ -124,6 +122,5 @@ struct VerifyCodeView: View {
|
||||
struct VerifyCodeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VerifyCodeView(displayName: "alice", connectionCode: "12345 67890 12345 67890", connectionVerified: false, verify: {_ in nil})
|
||||
.environmentObject(CurrentColors.toAppTheme())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ private let rowHeights: [DynamicTypeSize: CGFloat] = [
|
||||
|
||||
struct ChatListNavLink: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
||||
@ObservedObject var chat: Chat
|
||||
@State private var showContactRequestDialog = false
|
||||
@@ -225,7 +224,7 @@ struct ChatListNavLink: View {
|
||||
} label: {
|
||||
Label("Join", systemImage: chat.chatInfo.incognito ? "theatermasks" : "ipad.and.arrow.forward")
|
||||
}
|
||||
.tint(chat.chatInfo.incognito ? .indigo : theme.colors.primary)
|
||||
.tint(chat.chatInfo.incognito ? .indigo : .accentColor)
|
||||
}
|
||||
|
||||
@ViewBuilder private func markReadButton() -> some View {
|
||||
@@ -235,14 +234,14 @@ struct ChatListNavLink: View {
|
||||
} label: {
|
||||
Label("Read", systemImage: "checkmark")
|
||||
}
|
||||
.tint(theme.colors.primary)
|
||||
.tint(Color.accentColor)
|
||||
} else {
|
||||
Button {
|
||||
Task { await markChatUnread(chat) }
|
||||
} label: {
|
||||
Label("Unread", systemImage: "circlebadge.fill")
|
||||
}
|
||||
.tint(theme.colors.primary)
|
||||
.tint(Color.accentColor)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -307,7 +306,7 @@ struct ChatListNavLink: View {
|
||||
Button {
|
||||
Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) }
|
||||
} label: { Label("Accept", systemImage: "checkmark") }
|
||||
.tint(theme.colors.primary)
|
||||
.tint(.accentColor)
|
||||
Button {
|
||||
Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) }
|
||||
} label: {
|
||||
@@ -347,7 +346,7 @@ struct ChatListNavLink: View {
|
||||
} label: {
|
||||
Label("Name", systemImage: "pencil")
|
||||
}
|
||||
.tint(theme.colors.primary)
|
||||
.tint(.accentColor)
|
||||
}
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.appSheet(isPresented: $showContactConnectionInfo) {
|
||||
@@ -355,7 +354,6 @@ struct ChatListNavLink: View {
|
||||
if case let .contactConnection(contactConnection) = chat.chatInfo {
|
||||
ContactConnectionInfo(contactConnection: contactConnection)
|
||||
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import SimpleXChat
|
||||
|
||||
struct ChatListView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var showSettings: Bool
|
||||
@State private var searchMode = false
|
||||
@FocusState private var searchFocussed
|
||||
@@ -87,7 +86,6 @@ struct ChatListView: View {
|
||||
))
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.background(theme.colors.background)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarHidden(searchMode)
|
||||
.toolbar {
|
||||
@@ -145,14 +143,12 @@ struct ChatListView: View {
|
||||
searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink
|
||||
)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.clear)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
ForEach(cs, id: \.viewId) { chat in
|
||||
ChatListNavLink(chat: chat)
|
||||
.padding(.trailing, -16)
|
||||
.disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id))
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
.offset(x: -8)
|
||||
}
|
||||
@@ -164,7 +160,7 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
if cs.isEmpty && !chatModel.chats.isEmpty {
|
||||
Text("No filtered chats").foregroundColor(theme.colors.secondary)
|
||||
Text("No filtered chats").foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,7 +168,7 @@ struct ChatListView: View {
|
||||
private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View {
|
||||
Circle()
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
|
||||
private func onboardingButtons() -> some View {
|
||||
@@ -183,7 +179,7 @@ struct ChatListView: View {
|
||||
p.addLine(to: CGPoint(x: 0, y: 10))
|
||||
p.addLine(to: CGPoint(x: 8, y: 0))
|
||||
}
|
||||
.fill(theme.colors.primary)
|
||||
.fill(Color.accentColor)
|
||||
.frame(width: 20, height: 10)
|
||||
.padding(.trailing, 12)
|
||||
|
||||
@@ -193,7 +189,7 @@ struct ChatListView: View {
|
||||
|
||||
Spacer()
|
||||
Text("You have no chats")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding(.trailing, 6)
|
||||
@@ -206,7 +202,7 @@ struct ChatListView: View {
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.background(theme.colors.primary)
|
||||
.background(Color.accentColor)
|
||||
.foregroundColor(.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
@@ -271,7 +267,7 @@ struct SubsStatusIndicator: View {
|
||||
@State private var timer: Timer? = nil
|
||||
@State private var timerCounter = 0
|
||||
@State private var showServersSummary = false
|
||||
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false
|
||||
|
||||
// Constants for the intervals
|
||||
@@ -336,7 +332,6 @@ struct SubsStatusIndicator: View {
|
||||
|
||||
struct ChatListSearchBar: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var searchMode: Bool
|
||||
@FocusState.Binding var searchFocussed: Bool
|
||||
@Binding var searchText: String
|
||||
@@ -353,7 +348,7 @@ struct ChatListSearchBar: View {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
TextField("Search or paste SimpleX link", text: $searchText)
|
||||
.foregroundColor(searchShowingSimplexLink ? theme.colors.secondary : theme.colors.onBackground)
|
||||
.foregroundColor(searchShowingSimplexLink ? .secondary : .primary)
|
||||
.disabled(searchShowingSimplexLink)
|
||||
.focused($searchFocussed)
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -365,13 +360,13 @@ struct ChatListSearchBar: View {
|
||||
}
|
||||
}
|
||||
.padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.background(Color(.tertiarySystemFill))
|
||||
.cornerRadius(10.0)
|
||||
|
||||
if searchFocussed {
|
||||
Text("Cancel")
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.foregroundColor(.accentColor)
|
||||
.onTapGesture {
|
||||
searchText = ""
|
||||
searchFocussed = false
|
||||
@@ -422,7 +417,7 @@ struct ChatListSearchBar: View {
|
||||
Image(systemName: showUnreadAndFavorites ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.foregroundColor(showUnreadAndFavorites ? theme.colors.primary : theme.colors.secondary)
|
||||
.foregroundColor(showUnreadAndFavorites ? .accentColor : .secondary)
|
||||
.frame(width: showUnreadAndFavorites ? 22 : 16, height: showUnreadAndFavorites ? 22 : 16)
|
||||
.onTapGesture {
|
||||
showUnreadAndFavorites = !showUnreadAndFavorites
|
||||
|
||||
@@ -11,10 +11,10 @@ import SimpleXChat
|
||||
|
||||
struct ChatPreviewView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
@Binding var progressByTimeout: Bool
|
||||
@State var deleting: Bool = false
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
var darkGreen = Color(red: 0, green: 0.5, blue: 0)
|
||||
|
||||
@AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true
|
||||
@@ -36,7 +36,7 @@ struct ChatPreviewView: View {
|
||||
(cItem?.timestampText ?? formatTimestampText(chat.chatInfo.chatTs))
|
||||
.font(.subheadline)
|
||||
.frame(minWidth: 60, alignment: .trailing)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
.padding(.bottom, 4)
|
||||
@@ -94,9 +94,9 @@ struct ChatPreviewView: View {
|
||||
case let .group(groupInfo):
|
||||
let v = previewTitle(t)
|
||||
switch (groupInfo.membership.memberStatus) {
|
||||
case .memInvited: v.foregroundColor(deleting ? theme.colors.secondary : chat.chatInfo.incognito ? .indigo : theme.colors.primary)
|
||||
case .memAccepted: v.foregroundColor(theme.colors.secondary)
|
||||
default: if deleting { v.foregroundColor(theme.colors.secondary) } else { v }
|
||||
case .memInvited: v.foregroundColor(deleting ? .secondary : chat.chatInfo.incognito ? .indigo : .accentColor)
|
||||
case .memAccepted: v.foregroundColor(.secondary)
|
||||
default: if deleting { v.foregroundColor(.secondary) } else { v }
|
||||
}
|
||||
default: previewTitle(t)
|
||||
}
|
||||
@@ -108,7 +108,7 @@ struct ChatPreviewView: View {
|
||||
|
||||
private var verifiedIcon: Text {
|
||||
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.baselineOffset(1)
|
||||
.kerning(-2)
|
||||
}
|
||||
@@ -133,11 +133,11 @@ struct ChatPreviewView: View {
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 4)
|
||||
.frame(minWidth: 18, minHeight: 18)
|
||||
.background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? theme.colors.primary : theme.colors.secondary)
|
||||
.background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? Color.accentColor : Color.secondary)
|
||||
.cornerRadius(10)
|
||||
} else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local {
|
||||
Image(systemName: "speaker.slash.fill")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
} else if chat.chatInfo.chatSettings?.favorite ?? false {
|
||||
Image(systemName: "star.fill")
|
||||
.resizable()
|
||||
@@ -151,9 +151,9 @@ struct ChatPreviewView: View {
|
||||
|
||||
private func messageDraft(_ draft: ComposeState) -> Text {
|
||||
let msg = draft.message
|
||||
return image("rectangle.and.pencil.and.ellipsis", color: theme.colors.primary)
|
||||
return image("rectangle.and.pencil.and.ellipsis", color: .accentColor)
|
||||
+ attachment()
|
||||
+ messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary)
|
||||
+ messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true, showSecrets: false)
|
||||
|
||||
func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text {
|
||||
Text(Image(systemName: s)).foregroundColor(color) + Text(" ")
|
||||
@@ -172,7 +172,7 @@ struct ChatPreviewView: View {
|
||||
func chatItemPreview(_ cItem: ChatItem) -> Text {
|
||||
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText()
|
||||
let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil
|
||||
return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true, showSecrets: false, secondaryColor: theme.colors.secondary)
|
||||
return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true, showSecrets: false)
|
||||
|
||||
// same texts are in markedDeletedText in MarkedDeletedItemView, but it returns LocalizedStringKey;
|
||||
// can be refactored into a single function if functions calling these are changed to return same type
|
||||
@@ -206,7 +206,7 @@ struct ChatPreviewView: View {
|
||||
case let .direct(contact):
|
||||
if contact.activeConn == nil && contact.profile.contactLink != nil {
|
||||
chatPreviewInfoText("Tap to Connect")
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.foregroundColor(.accentColor)
|
||||
} else if !contact.ready && contact.activeConn != nil {
|
||||
if contact.nextSendGrpInv {
|
||||
chatPreviewInfoText("send direct message")
|
||||
@@ -257,38 +257,38 @@ struct ChatPreviewView: View {
|
||||
case let .direct(contact):
|
||||
if contact.active && contact.activeConn != nil {
|
||||
switch (chatModel.contactNetworkStatus(contact)) {
|
||||
case .connected: incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary)
|
||||
case .connected: incognitoIcon(chat.chatInfo.incognito)
|
||||
case .error:
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 17, height: 17)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
default:
|
||||
ProgressView()
|
||||
}
|
||||
} else {
|
||||
incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary)
|
||||
incognitoIcon(chat.chatInfo.incognito)
|
||||
}
|
||||
case .group:
|
||||
if progressByTimeout {
|
||||
ProgressView()
|
||||
} else {
|
||||
incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary)
|
||||
incognitoIcon(chat.chatInfo.incognito)
|
||||
}
|
||||
default:
|
||||
incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary)
|
||||
incognitoIcon(chat.chatInfo.incognito)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func incognitoIcon(_ incognito: Bool, _ secondaryColor: Color) -> some View {
|
||||
@ViewBuilder func incognitoIcon(_ incognito: Bool) -> some View {
|
||||
if incognito {
|
||||
Image(systemName: "theatermasks")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 22, height: 22)
|
||||
.foregroundColor(secondaryColor)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import SimpleXChat
|
||||
|
||||
struct ContactConnectionInfo: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@State var contactConnection: PendingContactConnection
|
||||
@State private var alert: CCInfoAlert?
|
||||
@@ -49,7 +48,7 @@ struct ContactConnectionInfo: View {
|
||||
|
||||
Section {
|
||||
if contactConnection.groupLinkId == nil {
|
||||
settingsRow("pencil", color: theme.colors.secondary) {
|
||||
settingsRow("pencil") {
|
||||
TextField("Set contact name…", text: $localAlias)
|
||||
.autocapitalization(.none)
|
||||
.autocorrectionDisabled(true)
|
||||
@@ -64,15 +63,14 @@ struct ContactConnectionInfo: View {
|
||||
let connReqInv = contactConnection.connReqInv {
|
||||
SimpleXLinkQRCode(uri: simplexChatLink(connReqInv))
|
||||
incognitoEnabled()
|
||||
shareLinkButton(connReqInv, theme.colors.secondary)
|
||||
oneTimeLinkLearnMoreButton(theme.colors.secondary)
|
||||
shareLinkButton(connReqInv)
|
||||
oneTimeLinkLearnMoreButton()
|
||||
} else {
|
||||
incognitoEnabled()
|
||||
oneTimeLinkLearnMoreButton(theme.colors.secondary)
|
||||
oneTimeLinkLearnMoreButton()
|
||||
}
|
||||
} footer: {
|
||||
sharedProfileInfo(contactConnection.incognito)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -84,7 +82,6 @@ struct ContactConnectionInfo: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
if #available(iOS 16, *) {
|
||||
v
|
||||
} else {
|
||||
@@ -152,7 +149,7 @@ struct ContactConnectionInfo: View {
|
||||
HStack(spacing: 6) {
|
||||
Text("Incognito")
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.foregroundColor(.accentColor)
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.onTapGesture {
|
||||
@@ -167,24 +164,23 @@ struct ContactConnectionInfo: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func shareLinkButton(_ connReqInvitation: String, _ secondaryColor: Color) -> some View {
|
||||
private func shareLinkButton(_ connReqInvitation: String) -> some View {
|
||||
Button {
|
||||
showShareSheet(items: [simplexChatLink(connReqInvitation)])
|
||||
} label: {
|
||||
settingsRow("square.and.arrow.up", color: secondaryColor) {
|
||||
settingsRow("square.and.arrow.up") {
|
||||
Text("Share 1-time link")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func oneTimeLinkLearnMoreButton(_ secondaryColor: Color) -> some View {
|
||||
private func oneTimeLinkLearnMoreButton() -> some View {
|
||||
NavigationLink {
|
||||
AddContactLearnMore(showTitle: false)
|
||||
.navigationTitle("One-time invitation link")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
settingsRow("info.circle", color: secondaryColor) {
|
||||
settingsRow("info.circle") {
|
||||
Text("Learn more")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import SimpleXChat
|
||||
struct ContactConnectionView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@State private var localAlias = ""
|
||||
@FocusState private var aliasTextFieldFocused: Bool
|
||||
@State private var showContactConnectionInfo = false
|
||||
@@ -30,7 +29,7 @@ struct ContactConnectionView: View {
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 48, height: 48)
|
||||
.foregroundColor(Color(uiColor: .tertiarySystemGroupedBackground).asAnotherColorFromSecondaryVariant(theme))
|
||||
.foregroundColor(Color(uiColor: .secondarySystemBackground))
|
||||
.onTapGesture { showContactConnectionInfo = true }
|
||||
}
|
||||
.frame(width: 63, height: 63)
|
||||
@@ -42,7 +41,7 @@ struct ContactConnectionView: View {
|
||||
.font(.title3)
|
||||
.bold()
|
||||
.allowsTightening(false)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.top, 1)
|
||||
.padding(.bottom, 0.5)
|
||||
@@ -55,14 +54,14 @@ struct ContactConnectionView: View {
|
||||
.padding(.trailing, 8)
|
||||
.padding(.vertical, 4)
|
||||
.frame(minWidth: 60, alignment: .trailing)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.bottom, 2)
|
||||
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Text(contactConnection.description)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
incognitoIcon(contactConnection.incognito, theme.colors.secondary)
|
||||
incognitoIcon(contactConnection.incognito)
|
||||
.padding(.top, 26)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import SimpleXChat
|
||||
|
||||
struct ContactRequestView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var contactRequest: UserContactRequest
|
||||
@ObservedObject var chat: Chat
|
||||
|
||||
@@ -24,7 +23,7 @@ struct ContactRequestView: View {
|
||||
Text(contactRequest.chatViewName)
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.foregroundColor(.accentColor)
|
||||
.padding(.leading, 8)
|
||||
.frame(alignment: .topLeading)
|
||||
Spacer()
|
||||
@@ -33,7 +32,7 @@ struct ContactRequestView: View {
|
||||
.padding(.trailing, 8)
|
||||
.padding(.top, 4)
|
||||
.frame(minWidth: 60, alignment: .trailing)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.bottom, 2)
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ struct ServersSummaryView: View {
|
||||
viewBody()
|
||||
.navigationTitle("Servers info")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
shareButton()
|
||||
@@ -265,7 +264,6 @@ struct ServersSummaryView: View {
|
||||
)
|
||||
.navigationBarTitle("SMP server")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
HStack {
|
||||
Text(serverAddress(srvSumm.smpServer))
|
||||
@@ -334,7 +332,6 @@ struct ServersSummaryView: View {
|
||||
)
|
||||
.navigationBarTitle("XFTP server")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
HStack {
|
||||
Text(serverAddress(srvSumm.xftpServer))
|
||||
@@ -363,7 +360,7 @@ struct ServersSummaryView: View {
|
||||
Button {
|
||||
alert = SomeAlert(
|
||||
alert: Alert(
|
||||
title: Text("Reset all statistics?"),
|
||||
title: Text("Reset all servers statistics?"),
|
||||
message: Text("Servers statistics will be reset - this cannot be undone!"),
|
||||
primaryButton: .destructive(Text("Reset")) {
|
||||
Task {
|
||||
@@ -425,7 +422,7 @@ struct SubscriptionStatusPercentageView: View {
|
||||
var body: some View {
|
||||
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||
let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, sess)
|
||||
Text(verbatim: "\(Int(floor(statusPercent * 100)))%")
|
||||
Text("\(Int(floor(statusPercent * 100)))%")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
@@ -473,7 +470,6 @@ struct SMPServerSummaryView: View {
|
||||
NavigationLink {
|
||||
ProtocolServersView(serverProtocol: .smp)
|
||||
.navigationTitle("Your SMP servers")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text("Open server settings")
|
||||
}
|
||||
@@ -567,7 +563,6 @@ struct SMPStatsView: View {
|
||||
DetailedSMPStatsView(stats: stats, statsStartedAt: statsStartedAt)
|
||||
.navigationTitle("Detailed statistics")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text("Details")
|
||||
}
|
||||
@@ -595,21 +590,21 @@ struct DetailedSMPStatsView: View {
|
||||
infoRowTwoValues("Sent via proxy", "attempts", stats._sentViaProxy, stats._sentViaProxyAttempts)
|
||||
infoRowTwoValues("Proxied", "attempts", stats._sentProxied, stats._sentProxiedAttempts)
|
||||
Text("Send errors")
|
||||
infoRow(Text(verbatim: "AUTH"), numOrDash(stats._sentAuthErrs)).padding(.leading, 24)
|
||||
infoRow(Text(verbatim: "QUOTA"), numOrDash(stats._sentQuotaErrs)).padding(.leading, 24)
|
||||
infoRow("expired", numOrDash(stats._sentExpiredErrs)).padding(.leading, 24)
|
||||
infoRow("other", numOrDash(stats._sentOtherErrs)).padding(.leading, 24)
|
||||
indentedInfoRow("AUTH", numOrDash(stats._sentAuthErrs))
|
||||
indentedInfoRow("QUOTA", numOrDash(stats._sentQuotaErrs))
|
||||
indentedInfoRow("expired", numOrDash(stats._sentExpiredErrs))
|
||||
indentedInfoRow("other", numOrDash(stats._sentOtherErrs))
|
||||
}
|
||||
Section("Received messages") {
|
||||
infoRow("Received total", numOrDash(stats._recvMsgs))
|
||||
Text("Receive errors")
|
||||
infoRow("duplicates", numOrDash(stats._recvDuplicates)).padding(.leading, 24)
|
||||
infoRow("decryption errors", numOrDash(stats._recvCryptoErrs)).padding(.leading, 24)
|
||||
infoRow("other errors", numOrDash(stats._recvErrs)).padding(.leading, 24)
|
||||
indentedInfoRow("duplicates", numOrDash(stats._recvDuplicates))
|
||||
indentedInfoRow("decryption errors", numOrDash(stats._recvCryptoErrs))
|
||||
indentedInfoRow("other errors", numOrDash(stats._recvErrs))
|
||||
infoRowTwoValues("Acknowledged", "attempts", stats._ackMsgs, stats._ackAttempts)
|
||||
Text("Acknowledgement errors")
|
||||
infoRow(Text(verbatim: "NO_MSG errors"), numOrDash(stats._ackNoMsgErrs)).padding(.leading, 24)
|
||||
infoRow("other errors", numOrDash(stats._ackOtherErrs)).padding(.leading, 24)
|
||||
indentedInfoRow("NO_MSG errors", numOrDash(stats._ackNoMsgErrs))
|
||||
indentedInfoRow("other errors", numOrDash(stats._ackOtherErrs))
|
||||
}
|
||||
Section {
|
||||
infoRow("Created", numOrDash(stats._connCreated))
|
||||
@@ -618,7 +613,7 @@ struct DetailedSMPStatsView: View {
|
||||
infoRowTwoValues("Deleted", "attempts", stats._connDeleted, stats._connDelAttempts)
|
||||
infoRow("Deletion errors", numOrDash(stats._connDelErrs))
|
||||
infoRowTwoValues("Subscribed", "attempts", stats._connSubscribed, stats._connSubAttempts)
|
||||
infoRow("Subscriptions ignored", numOrDash(stats._connSubIgnored))
|
||||
infoRow("Subscription results ignored", numOrDash(stats._connSubIgnored))
|
||||
infoRow("Subscription errors", numOrDash(stats._connSubErrs))
|
||||
} header: {
|
||||
Text("Connections")
|
||||
@@ -631,19 +626,29 @@ struct DetailedSMPStatsView: View {
|
||||
|
||||
private func infoRowTwoValues(_ title: LocalizedStringKey, _ title2: LocalizedStringKey, _ value: Int, _ value2: Int) -> some View {
|
||||
HStack {
|
||||
Text(title) + Text(verbatim: " / ").font(.caption2) + Text(title2).font(.caption2)
|
||||
Text(title) + Text(" / ").font(.caption2) + Text(title2).font(.caption2)
|
||||
Spacer()
|
||||
Group {
|
||||
if value == 0 && value2 == 0 {
|
||||
Text(verbatim: "-")
|
||||
Text("-")
|
||||
} else {
|
||||
Text(numOrDash(value)) + Text(verbatim: " / ").font(.caption2) + Text(numOrDash(value2)).font(.caption2)
|
||||
Text(numOrDash(value)) + Text(" / ").font(.caption2) + Text(numOrDash(value2)).font(.caption2)
|
||||
}
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
private func indentedInfoRow(_ title: LocalizedStringKey, _ value: String) -> some View {
|
||||
HStack {
|
||||
Text(title)
|
||||
.padding(.leading, 24)
|
||||
Spacer()
|
||||
Text(value)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
struct XFTPServerSummaryView: View {
|
||||
var summary: XFTPServerSummary
|
||||
var statsStartedAt: Date
|
||||
@@ -657,7 +662,6 @@ struct XFTPServerSummaryView: View {
|
||||
NavigationLink {
|
||||
ProtocolServersView(serverProtocol: .xftp)
|
||||
.navigationTitle("Your XFTP servers")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text("Open server settings")
|
||||
}
|
||||
@@ -688,7 +692,6 @@ struct XFTPStatsView: View {
|
||||
DetailedXFTPStatsView(stats: stats, statsStartedAt: statsStartedAt)
|
||||
.navigationTitle("Detailed statistics")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text("Details")
|
||||
}
|
||||
@@ -722,8 +725,8 @@ struct DetailedXFTPStatsView: View {
|
||||
infoRow("Size", prettySize(stats._downloadsSize))
|
||||
infoRowTwoValues("Chunks downloaded", "attempts", stats._downloads, stats._downloadAttempts)
|
||||
Text("Download errors")
|
||||
infoRow(Text(verbatim: "AUTH"), numOrDash(stats._downloadAuthErrs)).padding(.leading, 24)
|
||||
infoRow("other", numOrDash(stats._downloadErrs)).padding(.leading, 24)
|
||||
indentedInfoRow("AUTH", numOrDash(stats._downloadAuthErrs))
|
||||
indentedInfoRow("other", numOrDash(stats._downloadErrs))
|
||||
} header: {
|
||||
Text("Downloaded files")
|
||||
} footer: {
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
private let fillColorDark = Color(uiColor: UIColor(red: 0.11, green: 0.11, blue: 0.11, alpha: 255))
|
||||
private let fillColorLight = Color(uiColor: UIColor(red: 0.99, green: 0.99, blue: 0.99, alpha: 255))
|
||||
|
||||
struct UserPicker: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var showSettings: Bool
|
||||
@Binding var showConnectDesktop: Bool
|
||||
@Binding var userPickerVisible: Bool
|
||||
@@ -18,6 +21,9 @@ struct UserPicker: View {
|
||||
private let menuButtonHeight: CGFloat = 68
|
||||
@State var chatViewNameWidth: CGFloat = 0
|
||||
|
||||
var fillColor: Color {
|
||||
colorScheme == .dark ? fillColorDark : fillColorLight
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
@@ -76,7 +82,7 @@ struct UserPicker: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.background(
|
||||
Rectangle()
|
||||
.fill(theme.colors.surface)
|
||||
.fill(fillColor)
|
||||
.cornerRadius(16)
|
||||
.shadow(color: .black.opacity(0.12), radius: 24, x: 0, y: 0)
|
||||
)
|
||||
@@ -125,13 +131,13 @@ struct UserPicker: View {
|
||||
.padding(.trailing, 12)
|
||||
Text(user.chatViewName)
|
||||
.fontWeight(user.activeUser ? .medium : .regular)
|
||||
.foregroundColor(theme.colors.onBackground)
|
||||
.foregroundColor(.primary)
|
||||
.overlay(DetermineWidth())
|
||||
Spacer()
|
||||
if user.activeUser {
|
||||
Image(systemName: "checkmark")
|
||||
} else if u.unreadCount > 0 {
|
||||
unreadCounter(u.unreadCount, color: user.showNtfs ? theme.colors.primary : theme.colors.secondary)
|
||||
unreadCounter(u.unreadCount, color: user.showNtfs ? .accentColor : .secondary)
|
||||
} else if !user.showNtfs {
|
||||
Image(systemName: "speaker.slash")
|
||||
}
|
||||
@@ -139,7 +145,7 @@ struct UserPicker: View {
|
||||
.padding(.trailing)
|
||||
.padding([.leading, .vertical], 12)
|
||||
})
|
||||
.buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill)))
|
||||
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
|
||||
}
|
||||
|
||||
private func menuButton(_ title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View {
|
||||
@@ -150,13 +156,13 @@ struct UserPicker: View {
|
||||
Spacer()
|
||||
Image(systemName: icon)
|
||||
.symbolRenderingMode(.monochrome)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 22)
|
||||
.frame(height: menuButtonHeight)
|
||||
}
|
||||
.buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill)))
|
||||
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ChatArchiveView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var archiveName: String
|
||||
@AppStorage(DEFAULT_CHAT_ARCHIVE_NAME) private var chatArchiveName: String?
|
||||
@AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0
|
||||
@@ -21,14 +20,14 @@ struct ChatArchiveView: View {
|
||||
let fileTs = chatArchiveTimeDefault.get()
|
||||
List {
|
||||
Section {
|
||||
settingsRow("square.and.arrow.up", color: theme.colors.secondary) {
|
||||
settingsRow("square.and.arrow.up") {
|
||||
Button {
|
||||
showShareSheet(items: [fileUrl])
|
||||
} label: {
|
||||
Text("Save archive")
|
||||
}
|
||||
}
|
||||
settingsRow("trash", color: theme.colors.secondary) {
|
||||
settingsRow("trash") {
|
||||
Button {
|
||||
showDeleteAlert = true
|
||||
} label: {
|
||||
@@ -37,10 +36,8 @@ struct ChatArchiveView: View {
|
||||
}
|
||||
} header: {
|
||||
Text("Chat archive")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text("Created on \(fileTs)")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $showDeleteAlert) {
|
||||
|
||||
@@ -35,7 +35,6 @@ enum DatabaseEncryptionAlert: Identifiable {
|
||||
|
||||
struct DatabaseEncryptionView: View {
|
||||
@EnvironmentObject private var m: ChatModel
|
||||
@EnvironmentObject private var theme: AppTheme
|
||||
@Binding var useKeychain: Bool
|
||||
var migration: Bool
|
||||
@State private var alert: DatabaseEncryptionAlert? = nil
|
||||
@@ -64,7 +63,7 @@ struct DatabaseEncryptionView: View {
|
||||
|
||||
private func databaseEncryptionView() -> some View {
|
||||
Section {
|
||||
settingsRow(storedKey ? "key.fill" : "key", color: storedKey ? .green : theme.colors.secondary) {
|
||||
settingsRow(storedKey ? "key.fill" : "key", color: storedKey ? .green : .secondary) {
|
||||
Toggle("Save passphrase in Keychain", isOn: $useKeychainToggle)
|
||||
.onChange(of: useKeychainToggle) { _ in
|
||||
if useKeychainToggle {
|
||||
@@ -86,7 +85,7 @@ struct DatabaseEncryptionView: View {
|
||||
PassphraseField(key: $newKey, placeholder: "New passphrase…", valid: validKey(newKey), showStrength: true)
|
||||
PassphraseField(key: $confirmNewKey, placeholder: "Confirm new passphrase…", valid: confirmNewKey == "" || newKey == confirmNewKey)
|
||||
|
||||
settingsRow("lock.rotation", color: theme.colors.secondary) {
|
||||
settingsRow("lock.rotation") {
|
||||
Button(migration ? "Set passphrase" : "Update database passphrase") {
|
||||
alert = currentKey == ""
|
||||
? (useKeychain ? .encryptDatabaseSaved : .encryptDatabase)
|
||||
@@ -103,7 +102,6 @@ struct DatabaseEncryptionView: View {
|
||||
)
|
||||
} header: {
|
||||
Text(migration ? "Database passphrase" : "")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
if m.chatDbEncrypted == false {
|
||||
@@ -127,7 +125,6 @@ struct DatabaseEncryptionView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.padding(.top, 1)
|
||||
.font(.callout)
|
||||
}
|
||||
@@ -280,7 +277,6 @@ struct DatabaseEncryptionView: View {
|
||||
|
||||
|
||||
struct PassphraseField: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var key: String
|
||||
var placeholder: LocalizedStringKey
|
||||
var valid: Bool
|
||||
@@ -291,7 +287,7 @@ struct PassphraseField: View {
|
||||
var body: some View {
|
||||
ZStack(alignment: .leading) {
|
||||
let iconColor = valid
|
||||
? (showStrength && key != "" ? PassphraseStrength(passphrase: key).color : theme.colors.secondary)
|
||||
? (showStrength && key != "" ? PassphraseStrength(passphrase: key).color : .secondary)
|
||||
: .red
|
||||
Image(systemName: valid ? (showKey ? "eye.slash" : "eye") : "exclamationmark.circle")
|
||||
.resizable()
|
||||
|
||||
@@ -41,7 +41,6 @@ enum DatabaseAlert: Identifiable {
|
||||
|
||||
struct DatabaseView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var showSettings: Bool
|
||||
@State private var runChat = false
|
||||
@State private var alert: DatabaseAlert? = nil
|
||||
@@ -83,10 +82,8 @@ struct DatabaseView: View {
|
||||
.disabled(stopped || progressIndicator)
|
||||
} header: {
|
||||
Text("Messages")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text("This setting applies to messages in your current chat profile **\(m.currentUser?.displayName ?? "")**.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -108,27 +105,24 @@ struct DatabaseView: View {
|
||||
}
|
||||
} header: {
|
||||
Text("Run chat")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
if case .documents = dbContainer {
|
||||
Text("Database will be migrated when the app restarts")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
let unencrypted = m.chatDbEncrypted == false
|
||||
let color: Color = unencrypted ? .orange : theme.colors.secondary
|
||||
let color: Color = unencrypted ? .orange : .secondary
|
||||
settingsRow(unencrypted ? "lock.open" : useKeychain ? "key" : "lock", color: color) {
|
||||
NavigationLink {
|
||||
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false)
|
||||
.navigationTitle("Database passphrase")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text("Database passphrase")
|
||||
}
|
||||
}
|
||||
settingsRow("square.and.arrow.up", color: theme.colors.secondary) {
|
||||
settingsRow("square.and.arrow.up") {
|
||||
Button("Export database") {
|
||||
if initialRandomDBPassphraseGroupDefault.get() && !unencrypted {
|
||||
alert = .exportProhibited
|
||||
@@ -137,7 +131,7 @@ struct DatabaseView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
settingsRow("square.and.arrow.down", color: theme.colors.secondary) {
|
||||
settingsRow("square.and.arrow.down") {
|
||||
Button("Import database", role: .destructive) {
|
||||
showFileImporter = true
|
||||
}
|
||||
@@ -146,37 +140,34 @@ struct DatabaseView: View {
|
||||
let title: LocalizedStringKey = chatArchiveTimeDefault.get() < chatLastStartGroupDefault.get()
|
||||
? "Old database archive"
|
||||
: "New database archive"
|
||||
settingsRow("archivebox", color: theme.colors.secondary) {
|
||||
settingsRow("archivebox") {
|
||||
NavigationLink {
|
||||
ChatArchiveView(archiveName: archiveName)
|
||||
.navigationTitle(title)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text(title)
|
||||
}
|
||||
}
|
||||
}
|
||||
settingsRow("trash.slash", color: theme.colors.secondary) {
|
||||
settingsRow("trash.slash") {
|
||||
Button("Delete database", role: .destructive) {
|
||||
alert = .deleteChat
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Chat database")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text(
|
||||
stopped
|
||||
? "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts."
|
||||
: "Stop chat to enable database actions"
|
||||
)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
.disabled(!stopped)
|
||||
|
||||
if case .group = dbContainer, legacyDatabase {
|
||||
Section(header: Text("Old database").foregroundColor(theme.colors.secondary)) {
|
||||
settingsRow("trash", color: theme.colors.secondary) {
|
||||
Section("Old database") {
|
||||
settingsRow("trash") {
|
||||
Button("Delete old database") {
|
||||
alert = .deleteLegacyDatabase
|
||||
}
|
||||
@@ -191,15 +182,12 @@ struct DatabaseView: View {
|
||||
.disabled(!stopped || appFilesCountAndSize?.0 == 0)
|
||||
} header: {
|
||||
Text("Files & media")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
if let (fileCount, size) = appFilesCountAndSize {
|
||||
if fileCount == 0 {
|
||||
Text("No received or sent files")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} else {
|
||||
Text("\(fileCount) file(s) with total size of \(ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .binary))")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -367,7 +355,6 @@ struct DatabaseView: View {
|
||||
Task {
|
||||
do {
|
||||
try await apiDeleteStorage()
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
do {
|
||||
let config = ArchiveConfig(archivePath: archivePath.path)
|
||||
let archiveErrors = try await apiImportArchive(config: config)
|
||||
|
||||
@@ -189,7 +189,6 @@ struct MigrateToAppGroupView: View {
|
||||
Task {
|
||||
do {
|
||||
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
try await apiExportArchive(config: config)
|
||||
await MainActor.run { setV3DBMigration(.exported) }
|
||||
} catch let error {
|
||||
@@ -232,7 +231,6 @@ func exportChatArchive(_ storagePath: URL? = nil) async throws -> URL {
|
||||
if !ChatModel.shared.chatDbChanged {
|
||||
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||
}
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
try await apiExportArchive(config: config)
|
||||
if storagePath == nil {
|
||||
deleteOldArchive()
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ChatInfoImage: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ObservedObject var chat: Chat
|
||||
var size: CGFloat
|
||||
var color = Color(uiColor: .tertiarySystemGroupedBackground)
|
||||
@@ -24,7 +24,8 @@ struct ChatInfoImage: View {
|
||||
case .contactRequest: iconName = "person.crop.circle.fill"
|
||||
default: iconName = "circle.fill"
|
||||
}
|
||||
let iconColor = if case .local = chat.chatInfo { theme.appColors.primaryVariant2 } else { color }
|
||||
let notesColor = colorScheme == .light ? notesChatColorLight : notesChatColorDark
|
||||
let iconColor = if case .local = chat.chatInfo { notesColor } else { color }
|
||||
return ProfileImage(
|
||||
imageStr: chat.chatInfo.image,
|
||||
iconName: iconName,
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
//
|
||||
// ChatItemClipShape.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Levitating Pineapple on 04/07/2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
/// Modifier, which provides clipping mask for ``ChatItemWithMenu`` view
|
||||
/// and it's previews: (drag interaction, context menu, etc.)
|
||||
/// Supports [Dynamic Type](https://developer.apple.com/documentation/uikit/uifont/scaling_fonts_automatically)
|
||||
/// by retaining pill shape, even when ``ChatItem``'s height is less that twice its corner radius
|
||||
struct ChatItemClipped: ViewModifier {
|
||||
struct ClipShape: Shape {
|
||||
let maxCornerRadius: Double
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
Path(
|
||||
roundedRect: rect,
|
||||
cornerRadius: min((rect.height / 2), maxCornerRadius),
|
||||
style: .circular
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
clipShape = ClipShape(
|
||||
maxCornerRadius: 18
|
||||
)
|
||||
}
|
||||
|
||||
init(_ chatItem: ChatItem) {
|
||||
clipShape = ClipShape(
|
||||
maxCornerRadius: {
|
||||
switch chatItem.content {
|
||||
case
|
||||
.sndMsgContent,
|
||||
.rcvMsgContent,
|
||||
.rcvDecryptionError,
|
||||
.rcvGroupInvitation,
|
||||
.sndGroupInvitation,
|
||||
.sndDeleted,
|
||||
.rcvDeleted,
|
||||
.rcvIntegrityError,
|
||||
.sndModerated,
|
||||
.rcvModerated,
|
||||
.rcvBlocked,
|
||||
.invalidJSON: 18
|
||||
default: 8
|
||||
}
|
||||
}()
|
||||
)
|
||||
}
|
||||
|
||||
private let clipShape: ClipShape
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.contentShape(.dragPreview, clipShape)
|
||||
.contentShape(.contextMenuPreview, clipShape)
|
||||
.clipShape(clipShape)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
//
|
||||
// ChatWallpaper.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Avently on 14.06.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ChatViewBackground: ViewModifier {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var image: Image
|
||||
var imageType: WallpaperType
|
||||
var background: Color
|
||||
var tint: Color
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content.background(
|
||||
Canvas { context, size in
|
||||
var image = context.resolve(image)
|
||||
let rect = CGRectMake(0, 0, size.width, size.height)
|
||||
func repeatDraw(_ imageScale: CGFloat) {
|
||||
image.shading = .color(tint)
|
||||
let scale = imageScale * 1.57 // for some reason a wallpaper on iOS looks smaller than on Android
|
||||
for h in 0 ... Int(size.height / image.size.height / scale) {
|
||||
for w in 0 ... Int(size.width / image.size.width / scale) {
|
||||
let rect = CGRectMake(CGFloat(w) * image.size.width * scale, CGFloat(h) * image.size.height * scale, image.size.width * scale, image.size.height * scale)
|
||||
context.draw(image, in: rect, style: FillStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
context.fill(Path(rect), with: .color(background))
|
||||
switch imageType {
|
||||
case let WallpaperType.preset(filename, scale):
|
||||
repeatDraw(CGFloat((scale ?? 1) * (PresetWallpaper.from(filename)?.scale ?? 1)))
|
||||
case let WallpaperType.image(_, scale, scaleType):
|
||||
let scaleType = scaleType ?? WallpaperScaleType.fill
|
||||
switch scaleType {
|
||||
case WallpaperScaleType.repeat: repeatDraw(CGFloat(scale ?? 1))
|
||||
case WallpaperScaleType.fill: fallthrough
|
||||
case WallpaperScaleType.fit:
|
||||
let scale = scaleType.computeScaleFactor(image.size, size)
|
||||
let scaledWidth = (image.size.width * scale.0)
|
||||
let scaledHeight = (image.size.height * scale.1)
|
||||
context.draw(image, in: CGRectMake(((size.width - scaledWidth) / 2), ((size.height - scaledHeight) / 2), scaledWidth, scaledHeight), style: FillStyle())
|
||||
if case WallpaperScaleType.fit = scaleType {
|
||||
if scaledWidth < size.width {
|
||||
// has black lines at left and right sides
|
||||
var x = (size.width - scaledWidth) / 2
|
||||
while x > 0 {
|
||||
context.draw(image, in: CGRectMake((x - scaledWidth), ((size.height - scaledHeight) / 2), scaledWidth, scaledHeight), style: FillStyle())
|
||||
x -= scaledWidth
|
||||
}
|
||||
x = size.width - (size.width - scaledWidth) / 2
|
||||
while x < size.width {
|
||||
context.draw(image, in: CGRectMake(x, ((size.height - scaledHeight) / 2), scaledWidth, scaledHeight), style: FillStyle())
|
||||
|
||||
x += scaledWidth
|
||||
}
|
||||
} else {
|
||||
// has black lines at top and bottom sides
|
||||
var y = (size.height - scaledHeight) / 2
|
||||
while y > 0 {
|
||||
context.draw(image, in: CGRectMake(((size.width - scaledWidth) / 2), (y - scaledHeight), scaledWidth, scaledHeight), style: FillStyle())
|
||||
y -= scaledHeight
|
||||
}
|
||||
y = size.height - (size.height - scaledHeight) / 2
|
||||
while y < size.height {
|
||||
context.draw(image, in: CGRectMake(((size.width - scaledWidth) / 2), y, scaledWidth, scaledHeight), style: FillStyle())
|
||||
y += scaledHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
context.fill(Path(rect), with: .color(tint))
|
||||
}
|
||||
case WallpaperType.empty: ()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension PresetWallpaper {
|
||||
public func toType(_ base: DefaultTheme, _ scale: Float? = nil) -> WallpaperType {
|
||||
let scale = if let scale {
|
||||
scale
|
||||
} else if let type = ChatModel.shared.currentUser?.uiThemes?.preferredMode(base.mode == DefaultThemeMode.dark)?.wallpaper?.toAppWallpaper().type, type.sameType(WallpaperType.preset(filename, nil)) {
|
||||
type.scale
|
||||
} else if let scale = themeOverridesDefault.get().first(where: { $0.wallpaper != nil && $0.wallpaper!.preset == filename })?.wallpaper?.scale {
|
||||
scale
|
||||
} else {
|
||||
Float(1.0)
|
||||
}
|
||||
return WallpaperType.preset(
|
||||
filename,
|
||||
scale
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ struct LibraryMediaListPicker: UIViewControllerRepresentable {
|
||||
typealias UIViewControllerType = PHPickerViewController
|
||||
var addMedia: (_ content: UploadContent) async -> Void
|
||||
var selectionLimit: Int
|
||||
var filter: PHPickerFilter = .any(of: [.images, .videos])
|
||||
var finishedPreprocessing: () -> Void = {}
|
||||
var didFinishPicking: (_ didSelectItems: Bool) async -> Void
|
||||
|
||||
@@ -149,7 +148,7 @@ struct LibraryMediaListPicker: UIViewControllerRepresentable {
|
||||
|
||||
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||||
var config = PHPickerConfiguration()
|
||||
config.filter = filter
|
||||
config.filter = .any(of: [.images, .videos])
|
||||
config.selectionLimit = selectionLimit
|
||||
config.selection = .ordered
|
||||
config.preferredAssetRepresentationMode = .current
|
||||
|
||||
@@ -12,12 +12,10 @@ import SimpleXChat
|
||||
let defaultProfileImageCorner = 22.5
|
||||
|
||||
struct ProfileImage: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var imageStr: String? = nil
|
||||
var iconName: String = "person.crop.circle.fill"
|
||||
var size: CGFloat
|
||||
var color = Color(uiColor: .tertiarySystemGroupedBackground)
|
||||
var backgroundColor: Color? = nil
|
||||
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
|
||||
|
||||
var body: some View {
|
||||
@@ -26,12 +24,10 @@ struct ProfileImage: View {
|
||||
let uiImage = UIImage(data: data) {
|
||||
clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius)
|
||||
} else {
|
||||
let c = color.asAnotherColorFromSecondaryVariant(theme)
|
||||
Image(systemName: iconName)
|
||||
.resizable()
|
||||
.foregroundColor(c)
|
||||
.foregroundColor(color)
|
||||
.frame(width: size, height: size)
|
||||
.background(Circle().fill(backgroundColor != nil ? backgroundColor! : .clear))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,29 +51,6 @@ private let radiusFactor = (1 - squareToCircleRatio) / 50
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Color {
|
||||
func asAnotherColorFromSecondary(_ theme: AppTheme) -> Color {
|
||||
return self
|
||||
}
|
||||
|
||||
func asAnotherColorFromSecondaryVariant(_ theme: AppTheme) -> Color {
|
||||
let s = theme.colors.secondaryVariant
|
||||
let l = theme.colors.isLight
|
||||
return switch self {
|
||||
case Color(uiColor: .tertiaryLabel): // ChatView title
|
||||
l ? s.darker(0.05) : s.lighter(0.2)
|
||||
case Color(uiColor: .tertiarySystemFill): // SettingsView, ChatInfoView
|
||||
l ? s.darker(0.065) : s.lighter(0.085)
|
||||
case Color(uiColor: .quaternaryLabel): // ChatListView user picker
|
||||
l ? s.darker(0.1) : s.lighter(0.1)
|
||||
case Color(uiColor: .tertiarySystemGroupedBackground): // ChatListView items, forward view
|
||||
s.asGroupedBackground(theme.base.mode)
|
||||
default: self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProfileImage_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ProfileImage(imageStr: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAKHGlDQ1BJQ0MgUHJvZmlsZQAASImFVgdUVNcWve9Nb7QZeu9NehtAem/Sq6gMQ28OQxWxgAQjEFFEREARNFQFg1KjiIhiIQgoYA9IEFBisCAq6OQNJNH4//r/zDpz9ttzz7n73ffWmg0A6QCDxYqD+QCIT0hmezlYywQEBsngngEYCAIy0AC6DGYSy8rDwxUg8Xf9d7wbAxC33tHgzvrP3/9nCISFJzEBgIIRTGey2MkILkawT1oyi4tnEUxjI6IQvMLFkauYqxjQQtewwuoaHy8bBNMBwJMZDHYkAERbhJdJZUYic4hhCNZOCItOQDB3vjkzioFwxLsIXhcRl5IOAImrRzs+fivCk7QRrIL0shAcwNUW+tX8yH/tFfrPXgxG5D84Pi6F+dc9ck+HHJ7g641UMSQlQATQBHEgBaQDGcACbLAVYaIRJhx5Dv+9j77aZ4OsZIFtSEc0iARRIBnpt/9qlvfqpGSQBhjImnCEcUU+NtxnujZy4fbqVEiU/wuXdQyA9S0cDqfzC+e2F4DzyLkSB79wyi0A8KoBcL2GmcJOXePQ3C8MIAJeQAOiQArIAxXuWwMMgSmwBHbAGbgDHxAINgMmojceUZUGMkEWyAX54AA4DMpAJTgJ6sAZ0ALawQVwGVwDt8AQGAUPwQSYBi/AAngHliEIwkEUiAqJQtKQIqQO6UJ0yByyg1whLygQCoEioQQoBcqE9kD5UBFUBlVB9dBPUCd0GboBDUP3oUloDnoNfYRRMBmmwZKwEqwF02Er2AX2gTfBkXAinAHnwPvhUrgaPg23wZfhW/AoPAG/gBdRAEVCCaFkURooOsoG5Y4KQkWg2KidqDxUCaoa1YTqQvWj7qAmUPOoD2gsmoqWQWugTdGOaF80E52I3okuQJeh69Bt6D70HfQkegH9GUPBSGDUMSYYJ0wAJhKThsnFlGBqMK2Yq5hRzDTmHRaLFcIqY42wjthAbAx2O7YAewzbjO3BDmOnsIs4HE4Up44zw7njGLhkXC7uKO407hJuBDeNe48n4aXxunh7fBA+AZ+NL8E34LvxI/gZ/DKBj6BIMCG4E8II2wiFhFOELsJtwjRhmchPVCaaEX2IMcQsYimxiXiV+Ij4hkQiyZGMSZ6kaNJuUinpLOk6aZL0gSxAViPbkIPJKeT95FpyD/k++Q2FQlGiWFKCKMmU/ZR6yhXKE8p7HiqPJo8TTxjPLp5ynjaeEZ6XvAReRV4r3s28GbwlvOd4b/PO8xH4lPhs+Bh8O/nK+Tr5xvkW+an8Ovzu/PH8BfwN/Df4ZwVwAkoCdgJhAjkCJwWuCExRUVR5qg2VSd1DPUW9Sp2mYWnKNCdaDC2fdoY2SFsQFBDUF/QTTBcsF7woOCGEElISchKKEyoUahEaE/ooLClsJRwuvE+4SXhEeElEXMRSJFwkT6RZZFTko6iMqJ1orOhB0XbRx2JoMTUxT7E0seNiV8XmxWnipuJM8TzxFvEHErCEmoSXxHaJkxIDEouSUpIOkizJo5JXJOelhKQspWKkiqW6peakqdLm0tHSxdKXpJ/LCMpYycTJlMr0ySzISsg6yqbIVskOyi7LKcv5ymXLNcs9lifK0+Uj5Ivle+UXFKQV3BQyFRoVHigSFOmKUYpHFPsVl5SUlfyV9iq1K80qiyg7KWcoNyo/UqGoWKgkqlSr3FXFqtJVY1WPqQ6pwWoGalFq5Wq31WF1Q/Vo9WPqw+sw64zXJayrXjeuQdaw0kjVaNSY1BTSdNXM1mzXfKmloBWkdVCrX+uztoF2nPYp7Yc6AjrOOtk6XTqvddV0mbrlunf1KHr2erv0OvRe6avrh+sf179nQDVwM9hr0GvwydDIkG3YZDhnpGAUYlRhNE6n0T3oBfTrxhhja+NdxheMP5gYmiSbtJj8YaphGmvaYDq7Xnl9+PpT66fM5MwYZlVmE+Yy5iHmJ8wnLGQtGBbVFk8t5S3DLGssZ6xUrWKsTlu9tNa2Zlu3Wi/ZmNjssOmxRdk62ObZDtoJ2Pnaldk9sZezj7RvtF9wMHDY7tDjiHF0cTzoOO4k6cR0qndacDZy3uHc50J28XYpc3nqqubKdu1yg92c3Q65PdqguCFhQ7s7cHdyP+T+2EPZI9HjZ0+sp4dnueczLx2vTK9+b6r3Fu8G73c+1j6FPg99VXxTfHv9eP2C/er9lvxt/Yv8JwK0AnYE3AoUC4wO7AjCBfkF1QQtbrTbeHjjdLBBcG7w2CblTembbmwW2xy3+eIW3i2MLedCMCH+IQ0hKwx3RjVjMdQptCJ0gWnDPMJ8EWYZVhw2F24WXhQ+E2EWURQxG2kWeShyLsoiqiRqPtomuiz6VYxjTGXMUqx7bG0sJ84/rjkeHx8S35kgkBCb0LdVamv61mGWOiuXNZFokng4cYHtwq5JgpI2JXUk05A/0oEUlZTvUiZTzVPLU9+n+aWdS+dPT0gf2Ka2bd+2mQz7jB+3o7czt/dmymZmZU7usNpRtRPaGbqzd5f8rpxd07sddtdlEbNis37J1s4uyn67x39PV45kzu6cqe8cvmvM5cll547vNd1b+T36++jvB/fp7Tu673NeWN7NfO38kvyVAmbBzR90fij9gbM/Yv9goWHh8QPYAwkHxg5aHKwr4i/KKJo65HaorVimOK/47eEth2+U6JdUHiEeSTkyUepa2nFU4eiBoytlUWWj5dblzRUSFfsqlo6FHRs5bnm8qVKyMr/y44noE/eqHKraqpWqS05iT6aefHbK71T/j/Qf62vEavJrPtUm1E7UedX11RvV1zdINBQ2wo0pjXOng08PnbE909Gk0VTVLNScfxacTTn7/KeQn8ZaXFp6z9HPNZ1XPF/RSm3Na4PatrUttEe1T3QEdgx3Onf2dpl2tf6s+XPtBdkL5RcFLxZ2E7tzujmXMi4t9rB65i9HXp7q3dL78ErAlbt9nn2DV12uXr9mf+1Kv1X/petm1y/cMLnReZN+s/2W4a22AYOB1l8MfmkdNBxsu210u2PIeKhreP1w94jFyOU7tneu3XW6e2t0w+jwmO/YvfHg8Yl7Yfdm78fdf/Ug9cHyw92PMI/yHvM9Lnki8aT6V9VfmycMJy5O2k4OPPV++nCKOfXit6TfVqZznlGelcxIz9TP6s5emLOfG3q+8fn0C9aL5fnc3/l/r3ip8vL8H5Z/DCwELEy/Yr/ivC54I/qm9q3+295Fj8Un7+LfLS/lvRd9X/eB/qH/o//HmeW0FdxK6SfVT12fXT4/4sRzOCwGm7FqBVBIwhERALyuBYASCAB1CPEPG9f8119+BvrK2fyNwVndL5jhvubRVsMQgCakeCFp04OsQ1LJEgAe5NodqT6WANbT+yf/iqQIPd21PXgaAcDJcjivtwJAQHLFgcNZ9uBwPlUgYhHf1z37f7V9g9e8ITewiP88wfWIYET6HPg21nzjV2fybQVcxfrg2/onng/F50lD/ccAAAA4ZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAABigAwAEAAAAAQAAABgAAAAAwf1XlwAAAaNJREFUSA3FlT1LA0EQQBN/gYUYRTksJZVgEbCR/D+7QMr8ABtttBBCsLGzsLG2sxaxED/ie4d77u0dyaE5HHjczn7MzO7M7nU6/yXz+bwLhzCCjTQO+rZhDH3opuNLdRYN4RHe4RIKJ7R34Ro+4AEGSw2mE1iUwT18gpI74WvkGlccu4XNdH0jnYU7cAUacidn37qR23cOxc4aGU0nYUAn7iSWEHkz46w0ocdQu1X6B/AMQZ5o7KfBqNOfwRH8JB7FajGhnmcpKvQe3MEbvILiDm5gPXaCHnZr4vvFGMoEKudKn8YvQIOOe+YzCPop7dwJ3zRfJ7GDuso4YJGRa0yZgg4tUaNXdGrbuZWKKxzYYEJc2xp9AUUjGt8KC2jvgYadF8+10vJyDnNLXwbdiWUZi0fUK01Eoc+AZhCLZVzK4Vq6sDUdz+0dEcbbTTIOJmAyTVhx/WmvrExbv2jtPhWLKodjCtefZiEeZeVZWWSndgwj6fVf3XON8Qwq15++uoqrfYVrow6dGBpCq79ME291jaB0/Q2CPncyht/99MNO/vr9AqW/CGi8sJqbAAAAAElFTkSuQmCC", size: 63)
|
||||
|
||||
@@ -1,445 +0,0 @@
|
||||
//
|
||||
// ThemeModeEditor.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Avently on 20.06.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct UserWallpaperEditor: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var initialTheme: ThemeModeOverride
|
||||
@State var themeModeOverride: ThemeModeOverride
|
||||
@State var applyToMode: DefaultThemeMode?
|
||||
@State var showMore: Bool = false
|
||||
@Binding var globalThemeUsed: Bool
|
||||
var save: (DefaultThemeMode?, ThemeModeOverride?) async -> Void
|
||||
|
||||
@State private var showImageImporter: Bool = false
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
let wallpaperType = theme.wallpaper.type
|
||||
|
||||
WallpaperPresetSelector(
|
||||
selectedWallpaper: wallpaperType,
|
||||
currentColors: { type in
|
||||
// If applying for :
|
||||
// - all themes: no overrides needed
|
||||
// - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected
|
||||
let perUserOverride: ThemeModeOverrides? = wallpaperType.sameType(type) ? ChatModel.shared.currentUser?.uiThemes : nil
|
||||
return ThemeManager.currentColors(type, nil, perUserOverride, themeOverridesDefault.get())
|
||||
},
|
||||
onChooseType: onChooseType
|
||||
)
|
||||
.padding(.bottom, 10)
|
||||
.listRowInsets(.init())
|
||||
.listRowBackground(Color.clear)
|
||||
.modifier(WallpaperImporter(showImageImporter: $showImageImporter, onChooseImage: { image in
|
||||
if let filename = saveWallpaperFile(image: image) {
|
||||
_ = onTypeCopyFromSameTheme(WallpaperType.image(filename, 1, WallpaperScaleType.fill))
|
||||
}
|
||||
}))
|
||||
|
||||
WallpaperSetupView(
|
||||
wallpaperType: themeModeOverride.type,
|
||||
base: theme.base,
|
||||
initialWallpaper: theme.wallpaper,
|
||||
editColor: { name in editColor(name, theme) },
|
||||
onTypeChange: onTypeChange
|
||||
)
|
||||
|
||||
Section {
|
||||
if !globalThemeUsed {
|
||||
ResetToGlobalThemeButton(true, theme.colors.primary) {
|
||||
themeModeOverride = ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
globalThemeUsed = true
|
||||
Task {
|
||||
await save(applyToMode, nil)
|
||||
await MainActor.run {
|
||||
// Change accent color globally
|
||||
ThemeManager.applyTheme(currentThemeDefault.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SetDefaultThemeButton(theme.colors.primary) {
|
||||
globalThemeUsed = false
|
||||
let lightBase = DefaultTheme.LIGHT
|
||||
let darkBase = if theme.base != DefaultTheme.LIGHT { theme.base } else if systemDarkThemeDefault.get() == DefaultTheme.DARK.themeName { DefaultTheme.DARK } else if systemDarkThemeDefault.get() == DefaultTheme.BLACK.themeName { DefaultTheme.BLACK } else { DefaultTheme.SIMPLEX }
|
||||
let mode = themeModeOverride.mode
|
||||
Task {
|
||||
// Saving for both modes in one place by changing mode once per save
|
||||
if applyToMode == nil {
|
||||
let oppositeMode = mode == DefaultThemeMode.light ? DefaultThemeMode.dark : DefaultThemeMode.light
|
||||
await save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, oppositeMode == DefaultThemeMode.light ? lightBase : darkBase))
|
||||
}
|
||||
await MainActor.run {
|
||||
themeModeOverride = ThemeModeOverride.withFilledAppDefaults(mode, mode == DefaultThemeMode.light ? lightBase : darkBase)
|
||||
}
|
||||
await save(themeModeOverride.mode, themeModeOverride)
|
||||
await MainActor.run {
|
||||
// Change accent color globally
|
||||
ThemeManager.applyTheme(currentThemeDefault.get())
|
||||
}
|
||||
}
|
||||
}.onChange(of: initialTheme.mode) { mode in
|
||||
themeModeOverride = initialTheme
|
||||
if applyToMode != nil {
|
||||
applyToMode = mode
|
||||
}
|
||||
}
|
||||
.onChange(of: theme) { _ in
|
||||
// Applies updated global theme if current one tracks global theme
|
||||
if globalThemeUsed {
|
||||
themeModeOverride = ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
globalThemeUsed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if showMore {
|
||||
let values = [
|
||||
(nil, "All modes"),
|
||||
(DefaultThemeMode.light, "Light mode"),
|
||||
(DefaultThemeMode.dark, "Dark mode")
|
||||
]
|
||||
Picker("Apply to", selection: $applyToMode) {
|
||||
ForEach(values, id: \.0) { (_, text) in
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
.onChange(of: applyToMode) { mode in
|
||||
if let mode, mode != theme.base.mode {
|
||||
let lightBase = DefaultTheme.LIGHT
|
||||
let darkBase = if theme.base != DefaultTheme.LIGHT { theme.base } else if systemDarkThemeDefault.get() == DefaultTheme.DARK.themeName { DefaultTheme.DARK } else if systemDarkThemeDefault.get() == DefaultTheme.BLACK.themeName { DefaultTheme.BLACK } else { DefaultTheme.SIMPLEX }
|
||||
ThemeManager.applyTheme(mode == DefaultThemeMode.light ? lightBase.themeName : darkBase.themeName)
|
||||
}
|
||||
}
|
||||
|
||||
CustomizeThemeColorsSection(editColor: { name in editColor(name, theme) })
|
||||
|
||||
ImportExportThemeSection(perChat: nil, perUser: ChatModel.shared.currentUser?.uiThemes) { imported in
|
||||
let importedFromString = imported.wallpaper?.importFromString()
|
||||
let importedType = importedFromString?.toAppWallpaper().type
|
||||
let currentTheme = ThemeManager.currentColors(nil, nil, nil, themeOverridesDefault.get())
|
||||
let type: WallpaperType? = if importedType?.sameType(currentTheme.wallpaper.type) == true { nil } else { importedType }
|
||||
let colors = ThemeManager.currentThemeOverridesForExport(type, nil, nil).colors
|
||||
let res = ThemeModeOverride(mode: imported.base.mode, colors: imported.colors, wallpaper: importedFromString).removeSameColors(imported.base, colorsToCompare: colors)
|
||||
Task {
|
||||
await MainActor.run {
|
||||
themeModeOverride = res
|
||||
}
|
||||
await save(applyToMode, res)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AdvancedSettingsButton(theme.colors.primary) { showMore = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool {
|
||||
_ = ThemeManager.copyFromSameThemeOverrides(type, nil, $themeModeOverride)
|
||||
Task {
|
||||
await save(applyToMode, themeModeOverride)
|
||||
}
|
||||
globalThemeUsed = false
|
||||
return true
|
||||
}
|
||||
|
||||
private func preApplyGlobalIfNeeded(_ type: WallpaperType?) {
|
||||
if globalThemeUsed {
|
||||
_ = onTypeCopyFromSameTheme(type)
|
||||
}
|
||||
}
|
||||
|
||||
private func onTypeChange(_ type: WallpaperType?) {
|
||||
if globalThemeUsed {
|
||||
preApplyGlobalIfNeeded(type)
|
||||
// Saves copied static image instead of original from global theme
|
||||
ThemeManager.applyWallpaper(themeModeOverride.type, $themeModeOverride)
|
||||
} else {
|
||||
ThemeManager.applyWallpaper(type, $themeModeOverride)
|
||||
}
|
||||
Task {
|
||||
await save(applyToMode, themeModeOverride)
|
||||
}
|
||||
}
|
||||
|
||||
private func currentColors(_ type: WallpaperType?) -> ThemeManager.ActiveTheme {
|
||||
// If applying for :
|
||||
// - all themes: no overrides needed
|
||||
// - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected
|
||||
let perUserOverride: ThemeModeOverrides? = theme.wallpaper.type.sameType(type) ? ChatModel.shared.currentUser?.uiThemes : nil
|
||||
return ThemeManager.currentColors(type, nil, perUserOverride, themeOverridesDefault.get())
|
||||
}
|
||||
|
||||
private func onChooseType(_ type: WallpaperType?) {
|
||||
if let type, case WallpaperType.image = type {
|
||||
if theme.wallpaper.type.isImage || currentColors(type).wallpaper.type.image == nil {
|
||||
showImageImporter = true
|
||||
} else {
|
||||
_ = onTypeCopyFromSameTheme(currentColors(type).wallpaper.type)
|
||||
}
|
||||
} else if themeModeOverride.type != type || theme.wallpaper.type != type {
|
||||
_ = onTypeCopyFromSameTheme(type)
|
||||
} else {
|
||||
onTypeChange(type)
|
||||
}
|
||||
}
|
||||
|
||||
private func editColor(_ name: ThemeColor, _ currentTheme: AppTheme) -> Binding<Color> {
|
||||
editColorBinding(
|
||||
name: name,
|
||||
wallpaperType: theme.wallpaper.type,
|
||||
wallpaperImage: theme.wallpaper.type.image,
|
||||
theme: currentTheme,
|
||||
onColorChange: { color in
|
||||
preApplyGlobalIfNeeded(themeModeOverride.type)
|
||||
ThemeManager.applyThemeColor(name: name, color: color, pref: $themeModeOverride)
|
||||
Task { await save(applyToMode, themeModeOverride) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatWallpaperEditor: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@State private var currentTheme: ThemeManager.ActiveTheme
|
||||
var initialTheme: ThemeModeOverride
|
||||
@State var themeModeOverride: ThemeModeOverride
|
||||
@State var applyToMode: DefaultThemeMode?
|
||||
@State var showMore: Bool = false
|
||||
@Binding var globalThemeUsed: Bool
|
||||
var save: (DefaultThemeMode?, ThemeModeOverride?) async -> Void
|
||||
|
||||
@State private var showImageImporter: Bool = false
|
||||
|
||||
init(initialTheme: ThemeModeOverride, themeModeOverride: ThemeModeOverride, applyToMode: DefaultThemeMode? = nil, globalThemeUsed: Binding<Bool>, save: @escaping (DefaultThemeMode?, ThemeModeOverride?) async -> Void) {
|
||||
let cur = ThemeManager.currentColors(nil, globalThemeUsed.wrappedValue ? nil : themeModeOverride, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
self.currentTheme = cur
|
||||
self.initialTheme = initialTheme
|
||||
self.themeModeOverride = themeModeOverride
|
||||
self.applyToMode = applyToMode
|
||||
self._globalThemeUsed = globalThemeUsed
|
||||
self.save = save
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
WallpaperPresetSelector(
|
||||
selectedWallpaper: currentTheme.wallpaper.type,
|
||||
activeBackgroundColor: currentTheme.wallpaper.background,
|
||||
activeTintColor: currentTheme.wallpaper.tint,
|
||||
currentColors: currentColors,
|
||||
onChooseType: onChooseType
|
||||
)
|
||||
.padding(.bottom, 10)
|
||||
.listRowInsets(.init())
|
||||
.listRowBackground(Color.clear)
|
||||
.modifier(WallpaperImporter(showImageImporter: $showImageImporter, onChooseImage: { image in
|
||||
if let filename = saveWallpaperFile(image: image) {
|
||||
_ = onTypeCopyFromSameTheme(WallpaperType.image(filename, 1, WallpaperScaleType.fill))
|
||||
}
|
||||
}))
|
||||
|
||||
WallpaperSetupView(
|
||||
wallpaperType: themeModeOverride.type,
|
||||
base: currentTheme.base,
|
||||
initialWallpaper: currentTheme.wallpaper,
|
||||
editColor: editColor,
|
||||
onTypeChange: onTypeChange
|
||||
)
|
||||
|
||||
Section {
|
||||
if !globalThemeUsed {
|
||||
ResetToGlobalThemeButton(ChatModel.shared.currentUser?.uiThemes?.preferredMode(isInDarkTheme()) == nil, theme.colors.primary) {
|
||||
themeModeOverride = ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
globalThemeUsed = true
|
||||
Task {
|
||||
await save(applyToMode, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SetDefaultThemeButton(theme.colors.primary) {
|
||||
globalThemeUsed = false
|
||||
let lightBase = DefaultTheme.LIGHT
|
||||
let darkBase = if currentTheme.base != DefaultTheme.LIGHT { currentTheme.base } else if systemDarkThemeDefault.get() == DefaultTheme.DARK.themeName { DefaultTheme.DARK } else if systemDarkThemeDefault.get() == DefaultTheme.BLACK.themeName { DefaultTheme.BLACK } else { DefaultTheme.SIMPLEX }
|
||||
let mode = themeModeOverride.mode
|
||||
Task {
|
||||
// Saving for both modes in one place by changing mode once per save
|
||||
if applyToMode == nil {
|
||||
let oppositeMode = mode == DefaultThemeMode.light ? DefaultThemeMode.dark : DefaultThemeMode.light
|
||||
await save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, oppositeMode == DefaultThemeMode.light ? lightBase : darkBase))
|
||||
}
|
||||
await MainActor.run {
|
||||
themeModeOverride = ThemeModeOverride.withFilledAppDefaults(mode, mode == DefaultThemeMode.light ? lightBase : darkBase)
|
||||
}
|
||||
await save(themeModeOverride.mode, themeModeOverride)
|
||||
}
|
||||
}
|
||||
.onChange(of: initialTheme) { initial in
|
||||
if initial.mode != themeModeOverride.mode {
|
||||
themeModeOverride = initial
|
||||
currentTheme = ThemeManager.currentColors(nil, globalThemeUsed ? nil : themeModeOverride, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
if applyToMode != nil {
|
||||
applyToMode = initial.mode
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: currentTheme) { _ in
|
||||
// Applies updated global theme if current one tracks global theme
|
||||
if globalThemeUsed {
|
||||
themeModeOverride = ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
globalThemeUsed = true
|
||||
}
|
||||
}
|
||||
.onChange(of: themeModeOverride) { override in
|
||||
currentTheme = ThemeManager.currentColors(nil, globalThemeUsed ? nil : override, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
}
|
||||
}
|
||||
|
||||
if showMore {
|
||||
let values = [
|
||||
(nil, "All modes"),
|
||||
(DefaultThemeMode.light, "Light mode"),
|
||||
(DefaultThemeMode.dark, "Dark mode")
|
||||
]
|
||||
Picker("Apply to", selection: $applyToMode) {
|
||||
ForEach(values, id: \.0) { (_, text) in
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
.onChange(of: applyToMode) { mode in
|
||||
if let mode, mode != currentTheme.base.mode {
|
||||
let lightBase = DefaultTheme.LIGHT
|
||||
let darkBase = if currentTheme.base != DefaultTheme.LIGHT { currentTheme.base } else if systemDarkThemeDefault.get() == DefaultTheme.DARK.themeName { DefaultTheme.DARK } else if systemDarkThemeDefault.get() == DefaultTheme.BLACK.themeName { DefaultTheme.BLACK } else { DefaultTheme.SIMPLEX }
|
||||
ThemeManager.applyTheme(mode == DefaultThemeMode.light ? lightBase.themeName : darkBase.themeName)
|
||||
}
|
||||
}
|
||||
|
||||
CustomizeThemeColorsSection(editColor: editColor)
|
||||
|
||||
ImportExportThemeSection(perChat: themeModeOverride, perUser: ChatModel.shared.currentUser?.uiThemes) { imported in
|
||||
let importedFromString = imported.wallpaper?.importFromString()
|
||||
let importedType = importedFromString?.toAppWallpaper().type
|
||||
let currentTheme = ThemeManager.currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
let type: WallpaperType? = if importedType?.sameType(currentTheme.wallpaper.type) == true { nil } else { importedType }
|
||||
let colors = ThemeManager.currentThemeOverridesForExport(type, nil, ChatModel.shared.currentUser?.uiThemes).colors
|
||||
let res = ThemeModeOverride(mode: imported.base.mode, colors: imported.colors, wallpaper: importedFromString).removeSameColors(imported.base, colorsToCompare: colors)
|
||||
Task {
|
||||
await MainActor.run {
|
||||
themeModeOverride = res
|
||||
}
|
||||
await save(applyToMode, res)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AdvancedSettingsButton(theme.colors.primary) { showMore = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool {
|
||||
let success = ThemeManager.copyFromSameThemeOverrides(type, ChatModel.shared.currentUser?.uiThemes?.preferredMode(!currentTheme.colors.isLight), $themeModeOverride)
|
||||
if success {
|
||||
Task {
|
||||
await save(applyToMode, themeModeOverride)
|
||||
}
|
||||
globalThemeUsed = false
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
private func preApplyGlobalIfNeeded(_ type: WallpaperType?) {
|
||||
if globalThemeUsed {
|
||||
_ = onTypeCopyFromSameTheme(type)
|
||||
}
|
||||
}
|
||||
|
||||
private func onTypeChange(_ type: WallpaperType?) {
|
||||
if globalThemeUsed {
|
||||
preApplyGlobalIfNeeded(type)
|
||||
// Saves copied static image instead of original from global theme
|
||||
ThemeManager.applyWallpaper(themeModeOverride.type, $themeModeOverride)
|
||||
} else {
|
||||
ThemeManager.applyWallpaper(type, $themeModeOverride)
|
||||
}
|
||||
Task {
|
||||
await save(applyToMode, themeModeOverride)
|
||||
}
|
||||
}
|
||||
|
||||
private func currentColors(_ type: WallpaperType?) -> ThemeManager.ActiveTheme {
|
||||
// If applying for :
|
||||
// - all themes: no overrides needed
|
||||
// - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected
|
||||
let perChatOverride: ThemeModeOverride? = type?.sameType(themeModeOverride.type) == true ? themeModeOverride : nil
|
||||
return ThemeManager.currentColors(type, perChatOverride, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
}
|
||||
|
||||
private func onChooseType(_ type: WallpaperType?) {
|
||||
if let type, case WallpaperType.image = type {
|
||||
if (themeModeOverride.type?.isImage == true && !globalThemeUsed) || currentColors(type).wallpaper.type.image == nil {
|
||||
showImageImporter = true
|
||||
} else if !onTypeCopyFromSameTheme(currentColors(type).wallpaper.type) {
|
||||
showImageImporter = true
|
||||
}
|
||||
} else if globalThemeUsed || themeModeOverride.type != type || themeModeOverride.type != type {
|
||||
_ = onTypeCopyFromSameTheme(type)
|
||||
} else {
|
||||
onTypeChange(type)
|
||||
}
|
||||
}
|
||||
|
||||
private func editColor(_ name: ThemeColor) -> Binding<Color> {
|
||||
editColorBinding(
|
||||
name: name,
|
||||
wallpaperType: themeModeOverride.type,
|
||||
wallpaperImage: themeModeOverride.type?.image,
|
||||
theme: currentTheme.toAppTheme(),
|
||||
onColorChange: { color in
|
||||
preApplyGlobalIfNeeded(themeModeOverride.type)
|
||||
ThemeManager.applyThemeColor(name: name, color: color, pref: $themeModeOverride)
|
||||
currentTheme = ThemeManager.currentColors(nil, globalThemeUsed ? nil : themeModeOverride, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
Task { await save(applyToMode, themeModeOverride) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func ResetToGlobalThemeButton(_ app: Bool, _ primaryColor: Color, _ onClick: @escaping () -> Void) -> some View {
|
||||
Button {
|
||||
onClick()
|
||||
} label: {
|
||||
Text(app ? "Reset to app theme" : "Reset to user theme")
|
||||
.foregroundColor(primaryColor)
|
||||
}
|
||||
}
|
||||
|
||||
private func SetDefaultThemeButton(_ primaryColor: Color, _ onClick: @escaping () -> Void) -> some View {
|
||||
Button {
|
||||
onClick()
|
||||
} label: {
|
||||
Text("Set default theme")
|
||||
.foregroundColor(primaryColor)
|
||||
}
|
||||
}
|
||||
|
||||
private func AdvancedSettingsButton(_ primaryColor: Color, _ onClick: @escaping () -> Void) -> some View {
|
||||
Button {
|
||||
onClick()
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "chevron.down")
|
||||
Text("Advanced settings")
|
||||
}.foregroundColor(primaryColor)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// TranslateSheet.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Levitating Pineapple on 03/07/2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Translation
|
||||
|
||||
extension View {
|
||||
@ViewBuilder func translateSheet(text: Binding<String?>) -> some View {
|
||||
if #available(iOS 17.4, *) {
|
||||
translationPresentation(
|
||||
isPresented: Binding(
|
||||
get: { text.wrappedValue != nil },
|
||||
set: { if !$0 { text.wrappedValue = nil } }
|
||||
),
|
||||
text: text.wrappedValue ?? String()
|
||||
)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
//
|
||||
// ViewModifiers.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Avently on 12.06.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
@ViewBuilder func `if`<Content: View>(_ condition: @autoclosure () -> Bool, transform: (Self) -> Content) -> some View {
|
||||
if condition() {
|
||||
transform(self)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import SwiftUI
|
||||
|
||||
struct PasscodeEntry: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var width: CGFloat
|
||||
var height: CGFloat
|
||||
@Binding var password: String
|
||||
@@ -141,11 +140,11 @@ struct PasscodeEntry: View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.frame(width: h, height: h)
|
||||
.foregroundColor(AppTheme.shared.colors.background)
|
||||
.foregroundColor(Color(uiColor: .systemBackground))
|
||||
label()
|
||||
}
|
||||
}
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: size, height: h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ struct PasscodeView: View {
|
||||
}
|
||||
.padding(.horizontal, 40)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(AppTheme.shared.colors.background)
|
||||
.background(Color(uiColor: .systemBackground))
|
||||
}
|
||||
|
||||
private func verticalPasscodeView(_ g: GeometryProxy) -> some View {
|
||||
|
||||