Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed16f71ee4 | |||
| 5bc70bdfed | |||
| fb4927ca87 | |||
| 2023464a13 | |||
| b8bf5871fb |
@@ -158,19 +158,20 @@ We are prioritizing users privacy and security - it would be impossible without
|
||||
|
||||
Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure.
|
||||
|
||||
Your donations help us raise more funds - any amount, even the price of the cup of coffee, would make a big difference for us.
|
||||
Your donations help us raise more funds – any amount, even the price of the cup of coffee, would make a big difference for us.
|
||||
|
||||
It is possible to donate via:
|
||||
|
||||
- [GitHub](https://github.com/sponsors/simplex-chat) (commission-free) or [OpenCollective](https://opencollective.com/simplex-chat) (~10% commission).
|
||||
- Bitcoin: bc1qd74rc032ek2knhhr3yjq2ajzc5enz3h4qwnxad
|
||||
- [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us.
|
||||
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies.
|
||||
- Monero: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
|
||||
- BCH: bitcoincash:qq6c8vfvxqrk6rhdysgvkhqc24sggkfsx5nqvdlqcg
|
||||
- Ethereum: 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692
|
||||
- Bitcoin: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
|
||||
- BCH: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
|
||||
- USDT:
|
||||
- Ethereum: 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692
|
||||
- Solana: 7JCf5m3TiHmYKZVr6jCu1KeZVtb9Y1jRMQDU69p5ARnu
|
||||
- please ask if you want to donate any other coins.
|
||||
- BNB Smart Chain: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
|
||||
- Tron: TNnTrKLBmdy2Wn3cAQR98dAVvWhLskQGfW
|
||||
- Ethereum: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
|
||||
- Solana: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L
|
||||
|
||||
Thank you,
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -163,70 +162,6 @@ class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "wallpaper_cats@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_cats@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 60 KiB |
@@ -1,12 +1,10 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "wallpaper_flowers@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_flowers@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 173 KiB |
@@ -1,12 +1,10 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "wallpaper_hearts@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_hearts@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 97 KiB |
@@ -1,12 +1,10 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "wallpaper_kids@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_kids@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 86 KiB |
@@ -1,12 +1,10 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "wallpaper_school@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_school@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 248 KiB |
@@ -1,12 +1,10 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "wallpaper_travel@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_travel@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 86 KiB |
@@ -108,6 +108,8 @@ struct ContentView: View {
|
||||
initializationView()
|
||||
}
|
||||
}
|
||||
//.tint(theme.colors.primary)
|
||||
.background(theme.colors.background)
|
||||
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
|
||||
.sheet(isPresented: $showSettings) {
|
||||
SettingsView(showSettings: $showSettings)
|
||||
@@ -150,16 +152,10 @@ 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()
|
||||
if sceneDelegate.window?.overrideUserInterfaceStyle == .unspecified {
|
||||
reactOnDarkThemeChanges(scheme == .dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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?
|
||||
@@ -73,7 +69,6 @@ final class ChatModel: ObservableObject {
|
||||
var chatItemStatuses: Dictionary<Int64, CIStatus> = [:]
|
||||
@Published var chatToTop: String?
|
||||
@Published var groupMembers: [GMember] = []
|
||||
@Published var groupMembersIndexes: Dictionary<Int64, Int> = [:] // groupMemberId to index in groupMembers list
|
||||
// items in the terminal view
|
||||
@Published var showingTerminal = false
|
||||
@Published var terminalItems: [TerminalItem] = []
|
||||
@@ -181,18 +176,8 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func populateGroupMembersIndexes() {
|
||||
groupMembersIndexes.removeAll()
|
||||
for (i, member) in groupMembers.enumerated() {
|
||||
groupMembersIndexes[member.groupMemberId] = i
|
||||
}
|
||||
}
|
||||
|
||||
func getGroupMember(_ groupMemberId: Int64) -> GMember? {
|
||||
if let i = groupMembersIndexes[groupMemberId] {
|
||||
return groupMembers[i]
|
||||
}
|
||||
return nil
|
||||
groupMembers.first { $0.groupMemberId == groupMemberId }
|
||||
}
|
||||
|
||||
private func getChatIndex(_ id: String) -> Int? {
|
||||
@@ -347,12 +332,12 @@ final class ChatModel: ObservableObject {
|
||||
|
||||
private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
|
||||
if let i = getChatItemIndex(cItem) {
|
||||
withConditionalAnimation {
|
||||
withAnimation {
|
||||
_updateChatItem(at: i, with: cItem)
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
withConditionalAnimation(itemAnimation()) {
|
||||
withAnimation(itemAnimation()) {
|
||||
var ci = cItem
|
||||
if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
|
||||
ci.meta.itemStatus = status
|
||||
@@ -372,7 +357,7 @@ final class ChatModel: ObservableObject {
|
||||
|
||||
func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem, status: CIStatus? = nil) {
|
||||
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
|
||||
withConditionalAnimation {
|
||||
withAnimation {
|
||||
_updateChatItem(at: i, with: cItem)
|
||||
}
|
||||
} else if let status = status {
|
||||
@@ -537,13 +522,11 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||
// update preview
|
||||
decreaseUnreadCounter(cInfo)
|
||||
// update current chat
|
||||
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
|
||||
if reversedChatItems[i].isRcvNew {
|
||||
// update current chat
|
||||
markChatItemRead_(i)
|
||||
// update preview
|
||||
decreaseUnreadCounter(cInfo)
|
||||
}
|
||||
markChatItemRead_(i)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,17 +661,14 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
// update current chat
|
||||
if chatId == groupInfo.id {
|
||||
if let i = groupMembersIndexes[member.groupMemberId] {
|
||||
if let i = groupMembers.firstIndex(where: { $0.groupMemberId == member.groupMemberId }) {
|
||||
withAnimation(.default) {
|
||||
self.groupMembers[i].wrapped = member
|
||||
self.groupMembers[i].created = Date.now
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
withAnimation {
|
||||
groupMembers.append(GMember(member))
|
||||
groupMembersIndexes[member.groupMemberId] = groupMembers.count - 1
|
||||
}
|
||||
withAnimation { groupMembers.append(GMember(member)) }
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
@@ -716,7 +696,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? {
|
||||
@@ -753,8 +733,8 @@ struct NTFContactRequest {
|
||||
var chatId: String
|
||||
}
|
||||
|
||||
struct UnreadChatItemCounts: Equatable {
|
||||
var isNearBottom: Bool
|
||||
struct UnreadChatItemCounts {
|
||||
var totalBelow: Int
|
||||
var unreadBelow: Int
|
||||
}
|
||||
|
||||
|
||||
@@ -92,22 +92,18 @@ private func withBGTask<T>(bgDelay: Double? = nil, f: @escaping () -> T) -> T {
|
||||
return r
|
||||
}
|
||||
|
||||
func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil, log: Bool = true) -> ChatResponse {
|
||||
if log {
|
||||
logger.debug("chatSendCmd \(cmd.cmdType)")
|
||||
}
|
||||
func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil) -> ChatResponse {
|
||||
logger.debug("chatSendCmd \(cmd.cmdType)")
|
||||
let start = Date.now
|
||||
let resp = bgTask
|
||||
? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd, ctrl) }
|
||||
: sendSimpleXCmd(cmd, ctrl)
|
||||
if log {
|
||||
logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)")
|
||||
if case let .response(_, json) = resp {
|
||||
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
||||
}
|
||||
Task {
|
||||
await TerminalItems.shared.addCommand(start, cmd.obfuscated, resp)
|
||||
}
|
||||
logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)")
|
||||
if case let .response(_, json) = resp {
|
||||
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
||||
}
|
||||
Task {
|
||||
await TerminalItems.shared.addCommand(start, cmd.obfuscated, resp)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
@@ -307,10 +303,8 @@ private func apiChatsResponse(_ r: ChatResponse) throws -> [ChatData] {
|
||||
throw r
|
||||
}
|
||||
|
||||
let loadItemsPerPage = 50
|
||||
|
||||
func apiGetChat(type: ChatType, id: Int64, search: String = "") throws -> Chat {
|
||||
let r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: .last(count: loadItemsPerPage), search: search))
|
||||
let r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: .last(count: 50), search: search))
|
||||
if case let .apiChat(_, chat) = r { return Chat.init(chat) }
|
||||
throw r
|
||||
}
|
||||
@@ -541,11 +535,6 @@ func reconnectAllServers() async throws {
|
||||
try await sendCommandOkResp(.reconnectAllServers)
|
||||
}
|
||||
|
||||
func reconnectServer(smpServer: String) async throws {
|
||||
let userId = try currentUserId("reconnectServer")
|
||||
try await sendCommandOkResp(.reconnectServer(userId: userId, smpServer: smpServer))
|
||||
}
|
||||
|
||||
func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) async throws {
|
||||
try await sendCommandOkResp(.apiSetChatSettings(type: type, id: id, chatSettings: chatSettings))
|
||||
}
|
||||
@@ -1354,18 +1343,6 @@ func apiGetVersion() throws -> CoreVersionInfo {
|
||||
throw r
|
||||
}
|
||||
|
||||
func getAgentServersSummary() throws -> PresentedServersSummary {
|
||||
let userId = try currentUserId("getAgentServersSummary")
|
||||
let r = chatSendCmdSync(.getAgentServersSummary(userId: userId), log: false)
|
||||
if case let .agentServersSummary(_, serversSummary) = r { return serversSummary }
|
||||
logger.error("getAgentServersSummary error: \(String(describing: r))")
|
||||
throw r
|
||||
}
|
||||
|
||||
func resetAgentServersStats() async throws {
|
||||
try await sendCommandOkResp(.resetAgentServersStats)
|
||||
}
|
||||
|
||||
private func currentUserId(_ funcName: String) throws -> Int64 {
|
||||
if let userId = ChatModel.shared.currentUser?.userId {
|
||||
return userId
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// Theme.swift
|
||||
// ThemeShared.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Avently on 14.06.2024.
|
||||
@@ -10,14 +10,23 @@ import Foundation
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
var CurrentColors: ThemeManager.ActiveTheme = ThemeManager.currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
var CurrentColors: ThemeManager.ActiveTheme = ThemeManager.currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) {
|
||||
didSet {
|
||||
AppTheme.shared.name = CurrentColors.name
|
||||
AppTheme.shared.base = CurrentColors.base
|
||||
AppTheme.shared.colors.updateColorsFrom(CurrentColors.colors)
|
||||
AppTheme.shared.appColors.updateColorsFrom(CurrentColors.appColors)
|
||||
AppTheme.shared.wallpaper.updateWallpaperFrom(CurrentColors.wallpaper)
|
||||
AppTheme.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
class AppTheme: ObservableObject {
|
||||
static let shared = AppTheme(name: CurrentColors.name, base: CurrentColors.base, colors: CurrentColors.colors, appColors: CurrentColors.appColors, wallpaper: CurrentColors.wallpaper)
|
||||
|
||||
var name: String
|
||||
@@ -33,27 +42,10 @@ class AppTheme: ObservableObject, Equatable {
|
||||
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
|
||||
@@ -61,12 +53,8 @@ struct ThemedBackground: ViewModifier {
|
||||
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)
|
||||
theme.colors.background.lighter(0.4),
|
||||
theme.colors.background.darker(0.4)
|
||||
],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
@@ -80,19 +68,15 @@ struct ThemedBackground: ViewModifier {
|
||||
.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 {
|
||||
func reactOnDarkThemeChanges(_ isDark: Bool) {
|
||||
systemInDarkThemeCurrently = isDark
|
||||
//sceneDelegate.window?.overrideUserInterfaceStyle == .unspecified
|
||||
if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME && CurrentColors.colors.isLight == isDark {
|
||||
// Change active colors from light to dark and back based on system theme
|
||||
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
|
||||
}
|
||||
@@ -120,10 +104,10 @@ extension ThemeWallpaper {
|
||||
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 }
|
||||
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,
|
||||
@@ -135,66 +119,3 @@ extension ThemeWallpaper {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,16 +11,12 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
class ThemeManager {
|
||||
struct ActiveTheme: Equatable {
|
||||
struct ActiveTheme {
|
||||
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)
|
||||
}
|
||||
var wallpaper: AppWallpaper = AppWallpaper(background: nil, tint: nil, type: .Empty)
|
||||
}
|
||||
|
||||
private static func systemDarkThemeColors() -> (Colors, DefaultTheme) {
|
||||
@@ -75,9 +71,7 @@ class ThemeManager {
|
||||
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 {
|
||||
let presetWallpaperTheme: ThemeColors? = 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 }
|
||||
@@ -96,12 +90,12 @@ class ThemeManager {
|
||||
)
|
||||
}
|
||||
|
||||
static func currentThemeOverridesForExport(_ themeOverridesForType: WallpaperType?, _ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverrides?) -> ThemeOverrides {
|
||||
let current = currentColors(themeOverridesForType, perChatTheme, perUserTheme, themeOverridesDefault.get())
|
||||
static func currentThemeOverridesForExport(_ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverrides?) -> ThemeOverrides {
|
||||
let current = currentColors(nil, 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 {
|
||||
let w: ThemeWallpaper? = if case WallpaperType.Empty = wType {
|
||||
nil
|
||||
} else {
|
||||
ThemeWallpaper.from(wType, wBackground?.toReadableHex(), wTint?.toReadableHex()).withFilledWallpaperBase64()
|
||||
@@ -117,48 +111,18 @@ class ThemeManager {
|
||||
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
|
||||
SceneDelegate.windowStatic?.tintColor = UIColor(CurrentColors.colors.primary)
|
||||
SceneDelegate.windowStatic?.backgroundColor = UIColor(CurrentColors.colors.background)
|
||||
SceneDelegate.windowStatic?.overrideUserInterfaceStyle = switch currentThemeDefault.get() {
|
||||
case DefaultTheme.LIGHT.themeName: .light
|
||||
case DefaultTheme.SYSTEM_THEME_NAME: .unspecified
|
||||
default: .dark
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -171,7 +135,7 @@ class ThemeManager {
|
||||
var themeIds = currentThemeIdsDefault.get()
|
||||
themeIds[nonSystemThemeName] = prevValue.themeId
|
||||
currentThemeIdsDefault.set(themeIds)
|
||||
applyTheme(currentThemeDefault.get())
|
||||
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
}
|
||||
|
||||
static func applyThemeColor(name: ThemeColor, color: Color? = nil, pref: Binding<ThemeModeOverride>) {
|
||||
@@ -185,7 +149,7 @@ class ThemeManager {
|
||||
let theme = overrides.sameTheme(type, baseTheme.themeName)
|
||||
var prevValue = theme ?? ThemeOverrides(base: baseTheme)
|
||||
prevValue.wallpaper = if let type {
|
||||
if case WallpaperType.empty = type {
|
||||
if case WallpaperType.Empty = type {
|
||||
nil as ThemeWallpaper?
|
||||
} else {
|
||||
ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint)
|
||||
@@ -197,7 +161,7 @@ class ThemeManager {
|
||||
var themeIds = currentThemeIdsDefault.get()
|
||||
themeIds[nonSystemThemeName] = prevValue.themeId
|
||||
currentThemeIdsDefault.set(themeIds)
|
||||
applyTheme(nonSystemThemeName)
|
||||
CurrentColors = currentColors( nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
}
|
||||
|
||||
static func copyFromSameThemeOverrides(_ type: WallpaperType?, _ lowerLevelOverride: ThemeModeOverride?, _ pref: Binding<ThemeModeOverride>) -> Bool {
|
||||
@@ -212,8 +176,6 @@ class ThemeManager {
|
||||
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
|
||||
@@ -222,10 +184,10 @@ class ThemeManager {
|
||||
return true
|
||||
}
|
||||
var type = sameWallpaper.toAppWallpaper().type
|
||||
if case let WallpaperType.image(filename, scale, scaleType) = type, sameWallpaper.imageFile == filename {
|
||||
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)
|
||||
type = WallpaperType.Image(filename, scale, scaleType)
|
||||
} else {
|
||||
logger.error("Error while copying wallpaper from global overrides to chat overrides")
|
||||
return false
|
||||
@@ -235,8 +197,6 @@ class ThemeManager {
|
||||
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
|
||||
@@ -270,7 +230,7 @@ class ThemeManager {
|
||||
var currentThemeIds = currentThemeIdsDefault.get()
|
||||
currentThemeIds[nonSystemThemeName] = prevValue.themeId
|
||||
currentThemeIdsDefault.set(currentThemeIds)
|
||||
applyTheme(nonSystemThemeName)
|
||||
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
}
|
||||
|
||||
static func resetAllThemeColors(_ pref: CodableDefault<[ThemeOverrides]>? = nil) {
|
||||
@@ -284,7 +244,7 @@ class ThemeManager {
|
||||
prevValue.wallpaper?.background = nil
|
||||
prevValue.wallpaper?.tint = nil
|
||||
pref.set(overrides.replace(prevValue))
|
||||
applyTheme(currentThemeDefault.get())
|
||||
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
}
|
||||
|
||||
static func resetAllThemeColors(_ pref: Binding<ThemeModeOverride>) {
|
||||
|
||||
@@ -44,7 +44,7 @@ struct IncomingCallView: View {
|
||||
cc.endCall(invitation: invitation)
|
||||
}
|
||||
|
||||
callButton("Ignore", "multiply", .primary) {
|
||||
callButton("Ignore", "multiply", theme.colors.primary) {
|
||||
cc.activeCallInvitation = nil
|
||||
}
|
||||
|
||||
@@ -77,7 +77,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)
|
||||
}
|
||||
}
|
||||
@@ -144,7 +144,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 +162,6 @@ struct ChatInfoView: View {
|
||||
connStats.ratchetSyncAllowed {
|
||||
synchronizeConnectionButton()
|
||||
}
|
||||
NavigationLink {
|
||||
ChatWallpaperEditorSheet(chat: chat)
|
||||
} label: {
|
||||
Label("Chat theme", systemImage: "photo")
|
||||
}
|
||||
// } else if developerTools {
|
||||
// synchronizeConnectionButtonForce()
|
||||
// }
|
||||
@@ -189,15 +184,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 +212,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 +224,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 +242,7 @@ struct ChatInfoView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
@@ -301,7 +294,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 +334,7 @@ struct ChatInfoView: View {
|
||||
setContactAlias()
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
private func setContactAlias() {
|
||||
@@ -379,7 +372,7 @@ struct ChatInfoView: View {
|
||||
)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("Security code")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Label(
|
||||
contact.verified ? "View security code" : "Verify security code",
|
||||
@@ -396,7 +389,7 @@ struct ChatInfoView: View {
|
||||
currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences)
|
||||
)
|
||||
.navigationBarTitle("Contact preferences")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
Label("Contact preferences", systemImage: "switch.2")
|
||||
@@ -449,7 +442,7 @@ struct ChatInfoView: View {
|
||||
.font(.system(size: 14))
|
||||
Spacer()
|
||||
Text(chatModel.contactNetworkStatus(contact).statusString)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
serverImage()
|
||||
}
|
||||
}
|
||||
@@ -457,7 +450,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 +569,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?"),
|
||||
|
||||
@@ -23,7 +23,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()
|
||||
}
|
||||
@@ -53,7 +53,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 +71,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,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,7 +41,7 @@ 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)
|
||||
@@ -51,7 +51,7 @@ struct CIFeaturePreferenceView: View {
|
||||
}
|
||||
r = r + chatItem.timestampText
|
||||
.fontWeight(.light)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
return r.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CIFileView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
let file: CIFile?
|
||||
let edited: Bool
|
||||
@@ -30,12 +31,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)
|
||||
|
||||
@@ -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 : theme.colors.primary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
|
||||
@@ -70,6 +70,7 @@ struct CIGroupInvitationView: View {
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(chatItemFrameColor(chatItem, theme))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
|
||||
.onChange(of: inProgress) { inProgress in
|
||||
|
||||
@@ -11,10 +11,12 @@ import SimpleXChat
|
||||
|
||||
struct CIImageView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
let chatItem: ChatItem
|
||||
var preview: UIImage?
|
||||
let image: String
|
||||
let maxWidth: CGFloat
|
||||
var imgWidth: CGFloat?
|
||||
@Binding var imgWidth: CGFloat?
|
||||
@State var scrollProxy: ScrollViewProxy?
|
||||
@State private var showFullScreenImage = false
|
||||
|
||||
var body: some View {
|
||||
@@ -23,14 +25,15 @@ struct CIImageView: View {
|
||||
if let uiImage = getLoadedImage(file) {
|
||||
imageView(uiImage)
|
||||
.fullScreenCover(isPresented: $showFullScreenImage) {
|
||||
FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage)
|
||||
FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage, scrollProxy: scrollProxy)
|
||||
}
|
||||
.onTapGesture { showFullScreenImage = true }
|
||||
.onChange(of: m.activeCallViewIsCollapsed) { _ in
|
||||
showFullScreenImage = false
|
||||
}
|
||||
} else if let preview {
|
||||
imageView(preview)
|
||||
} else if let data = Data(base64Encoded: dropImagePrefix(image)),
|
||||
let uiImage = UIImage(data: data) {
|
||||
imageView(uiImage)
|
||||
.onTapGesture {
|
||||
if let file = file {
|
||||
switch file.fileStatus {
|
||||
@@ -87,6 +90,7 @@ struct CIImageView: View {
|
||||
|
||||
private func imageView(_ img: UIImage) -> some View {
|
||||
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth
|
||||
DispatchQueue.main.async { imgWidth = w }
|
||||
return ZStack(alignment: .topTrailing) {
|
||||
if img.imageData == nil {
|
||||
Image(uiImage: img)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -49,7 +49,7 @@ struct CIMemberCreatedContactView: View {
|
||||
}
|
||||
r = r + chatItem.timestampText
|
||||
.fontWeight(.light)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
return r.font(.caption)
|
||||
}
|
||||
|
||||
@@ -57,11 +57,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +132,7 @@ struct CIRcvDecryptionError: View {
|
||||
.onTapGesture(perform: { onClick() })
|
||||
.padding(.vertical, 6)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
||||
@@ -151,6 +152,7 @@ struct CIRcvDecryptionError: View {
|
||||
.onTapGesture(perform: { onClick() })
|
||||
.padding(.vertical, 6)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,13 +13,16 @@ import Combine
|
||||
|
||||
struct CIVideoView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
private let chatItem: ChatItem
|
||||
private let preview: UIImage?
|
||||
private let image: String
|
||||
@State private var duration: Int
|
||||
@State private var progress: Int = 0
|
||||
@State private var videoPlaying: Bool = false
|
||||
private let maxWidth: CGFloat
|
||||
private var videoWidth: CGFloat?
|
||||
@Binding private var videoWidth: CGFloat?
|
||||
@State private var scrollProxy: ScrollViewProxy?
|
||||
@State private var preview: UIImage? = nil
|
||||
@State private var player: AVPlayer?
|
||||
@State private var fullPlayer: AVPlayer?
|
||||
@State private var url: URL?
|
||||
@@ -30,12 +33,13 @@ struct CIVideoView: View {
|
||||
@State private var fullScreenTimeObserver: Any? = nil
|
||||
@State private var publisher: AnyCancellable? = nil
|
||||
|
||||
init(chatItem: ChatItem, preview: UIImage?, duration: Int, maxWidth: CGFloat, videoWidth: CGFloat?) {
|
||||
init(chatItem: ChatItem, image: String, duration: Int, maxWidth: CGFloat, videoWidth: Binding<CGFloat?>, scrollProxy: ScrollViewProxy?) {
|
||||
self.chatItem = chatItem
|
||||
self.preview = preview
|
||||
self.image = image
|
||||
self._duration = State(initialValue: duration)
|
||||
self.maxWidth = maxWidth
|
||||
self.videoWidth = videoWidth
|
||||
self._videoWidth = videoWidth
|
||||
self.scrollProxy = scrollProxy
|
||||
if let url = getLoadedVideo(chatItem.file) {
|
||||
let decrypted = chatItem.file?.fileSource?.cryptoArgs == nil ? url : chatItem.file?.fileSource?.decryptedGet()
|
||||
self._urlDecrypted = State(initialValue: decrypted)
|
||||
@@ -45,6 +49,10 @@ struct CIVideoView: View {
|
||||
}
|
||||
self._url = State(initialValue: url)
|
||||
}
|
||||
if let data = Data(base64Encoded: dropImagePrefix(image)),
|
||||
let uiImage = UIImage(data: data) {
|
||||
self._preview = State(initialValue: uiImage)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -55,8 +63,9 @@ struct CIVideoView: View {
|
||||
videoView(player, decrypted, file, preview, duration)
|
||||
} else if let file = file, let defaultPreview = preview, file.loaded && urlDecrypted == nil {
|
||||
videoViewEncrypted(file, defaultPreview, duration)
|
||||
} else if let preview {
|
||||
imageView(preview)
|
||||
} else if let data = Data(base64Encoded: dropImagePrefix(image)),
|
||||
let uiImage = UIImage(data: data) {
|
||||
imageView(uiImage)
|
||||
.onTapGesture {
|
||||
if let file = file {
|
||||
switch file.fileStatus {
|
||||
@@ -143,6 +152,7 @@ struct CIVideoView: View {
|
||||
|
||||
private func videoView(_ player: AVPlayer, _ url: URL, _ file: CIFile, _ preview: UIImage, _ duration: Int) -> some View {
|
||||
let w = preview.size.width <= preview.size.height ? maxWidth * 0.75 : maxWidth
|
||||
DispatchQueue.main.async { videoWidth = w }
|
||||
return ZStack(alignment: .topTrailing) {
|
||||
ZStack(alignment: .center) {
|
||||
let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local)
|
||||
@@ -242,6 +252,7 @@ struct CIVideoView: View {
|
||||
|
||||
private func imageView(_ img: UIImage) -> some View {
|
||||
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth
|
||||
DispatchQueue.main.async { videoWidth = w }
|
||||
return ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: img)
|
||||
.resizable()
|
||||
|
||||
@@ -73,7 +73,7 @@ struct CIVoiceView: View {
|
||||
playbackState: $playbackState,
|
||||
playbackTime: $playbackTime
|
||||
)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
private func playbackSlider() -> some View {
|
||||
@@ -90,7 +90,6 @@ struct CIVoiceView: View {
|
||||
allowMenu = true
|
||||
}
|
||||
}
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
|
||||
private func metaView() -> some View {
|
||||
@@ -218,26 +217,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()
|
||||
@@ -263,7 +262,7 @@ struct VoiceMessagePlayer: View {
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
playPauseIcon(icon, theme.colors.primary)
|
||||
playPauseIcon(icon)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ struct DeletedItemView: View {
|
||||
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)
|
||||
.padding(.horizontal, 12)
|
||||
@@ -25,6 +25,7 @@ struct DeletedItemView: View {
|
||||
.padding(.leading, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(chatItemFrameColor(chatItem, theme))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -12,16 +12,15 @@ import SimpleXChat
|
||||
struct FramedItemView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@EnvironmentObject var scrollModel: ReverseListScrollModel<ChatItem>
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var preview: UIImage?
|
||||
@Binding var revealed: Bool
|
||||
var maxWidth: CGFloat = .infinity
|
||||
@State var scrollProxy: ScrollViewProxy? = nil
|
||||
@State var msgWidth: CGFloat = 0
|
||||
var imgWidth: CGFloat? = nil
|
||||
var videoWidth: CGFloat? = nil
|
||||
@State private var useWhiteMetaColor: Bool = false
|
||||
@State var imgWidth: CGFloat? = nil
|
||||
@State var videoWidth: CGFloat? = nil
|
||||
@State var metaColor = Color.secondary
|
||||
@State var showFullScreenImage = false
|
||||
@Binding var allowMenu: Bool
|
||||
@State private var showSecrets = false
|
||||
@@ -52,9 +51,10 @@ struct FramedItemView: View {
|
||||
if let qi = chatItem.quotedItem {
|
||||
ciQuoteView(qi)
|
||||
.onTapGesture {
|
||||
if let ci = m.reversedChatItems.first(where: { $0.id == qi.itemId }) {
|
||||
if let proxy = scrollProxy,
|
||||
let ci = m.reversedChatItems.first(where: { $0.id == qi.itemId }) {
|
||||
withAnimation {
|
||||
scrollModel.scrollToItem(id: ci.id)
|
||||
proxy.scrollTo(ci.viewId, anchor: .bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,16 +66,21 @@ 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("")
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
metaColor = metaColor == .secondary ? theme.colors.secondary : metaColor
|
||||
}
|
||||
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, theme))
|
||||
.cornerRadius(18)
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
|
||||
|
||||
if let (title, text) = chatItem.meta.itemStatus.statusInfo {
|
||||
@@ -105,33 +110,29 @@ struct FramedItemView: View {
|
||||
.padding(.bottom, 2)
|
||||
} else {
|
||||
switch (chatItem.content.msgContent) {
|
||||
case let .image(text, _):
|
||||
CIImageView(chatItem: chatItem, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth)
|
||||
case let .image(text, image):
|
||||
CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy)
|
||||
.overlay(DetermineWidth())
|
||||
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)
|
||||
}
|
||||
case let .video(text, _, duration):
|
||||
CIVideoView(chatItem: chatItem, preview: preview, duration: duration, maxWidth: maxWidth, videoWidth: videoWidth)
|
||||
case let .video(text, image, duration):
|
||||
CIVideoView(chatItem: chatItem, image: image, duration: duration, maxWidth: maxWidth, videoWidth: $videoWidth, scrollProxy: scrollProxy)
|
||||
.overlay(DetermineWidth())
|
||||
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)
|
||||
}
|
||||
@@ -342,6 +343,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 == ""
|
||||
|
||||
@@ -13,12 +13,12 @@ import AVKit
|
||||
|
||||
struct FullScreenMediaView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var scrollModel: ReverseListScrollModel<ChatItem>
|
||||
@State var chatItem: ChatItem
|
||||
@State var image: UIImage?
|
||||
@State var player: AVPlayer? = nil
|
||||
@State var url: URL? = nil
|
||||
@Binding var showView: Bool
|
||||
@State var scrollProxy: ScrollViewProxy?
|
||||
@State private var showNext = false
|
||||
@State private var nextImage: UIImage?
|
||||
@State private var nextPlayer: AVPlayer?
|
||||
@@ -71,7 +71,9 @@ struct FullScreenMediaView: View {
|
||||
let w = abs(t.width)
|
||||
if t.height > 60 && t.height > w * 2 {
|
||||
showView = false
|
||||
scrollModel.scrollToItem(id: chatItem.id)
|
||||
if let proxy = scrollProxy {
|
||||
proxy.scrollTo(chatItem.viewId)
|
||||
}
|
||||
} else if w > 60 && w > abs(t.height) * 2 && !scrolling {
|
||||
let previous = t.width > 0
|
||||
scrolling = true
|
||||
|
||||
@@ -70,6 +70,7 @@ struct CIMsgError: View {
|
||||
.padding(.leading, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
.onTapGesture(perform: onTap)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
.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
|
||||
@@ -20,9 +19,6 @@ struct ChatItemForwardingView: View {
|
||||
|
||||
@State private var searchText: String = ""
|
||||
@FocusState private var searchFocused
|
||||
@State private var alert: SomeAlert?
|
||||
@State private var hasSimplexLink_: Bool?
|
||||
private let chatsToForwardTo = filterChatsToForwardTo()
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
@@ -40,23 +36,31 @@ struct ChatItemForwardingView: View {
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
.alert(item: $alert) { $0.alert }
|
||||
}
|
||||
|
||||
@ViewBuilder private func forwardListView() -> some View {
|
||||
VStack(alignment: .leading) {
|
||||
let chatsToForwardTo = filterChatsToForwardTo()
|
||||
if !chatsToForwardTo.isEmpty {
|
||||
List {
|
||||
searchFieldView(text: $searchText, focussed: $searchFocused, theme.colors.onBackground, theme.colors.secondary)
|
||||
.padding(.leading, 2)
|
||||
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
|
||||
let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { foundChat($0, s) }
|
||||
ForEach(chats) { chat in
|
||||
forwardListChatView(chat)
|
||||
.disabled(chatModel.deletedChats.contains(chat.chatInfo.id))
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 8) {
|
||||
searchFieldView(text: $searchText, focussed: $searchFocused)
|
||||
.padding(.leading, 2)
|
||||
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
|
||||
let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { filterChatSearched($0, s) }
|
||||
ForEach(chats) { chat in
|
||||
Divider()
|
||||
forwardListNavLinkView(chat)
|
||||
.disabled(chatModel.deletedChats.contains(chat.chatInfo.id))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color(uiColor: .systemBackground))
|
||||
.cornerRadius(12)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
} else {
|
||||
ZStack {
|
||||
emptyList()
|
||||
@@ -67,7 +71,16 @@ struct ChatItemForwardingView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func foundChat(_ chat: Chat, _ searchStr: String) -> Bool {
|
||||
private func filterChatsToForwardTo() -> [Chat] {
|
||||
var filteredChats = chatModel.chats.filter({ canForwardToChat($0) })
|
||||
if let index = filteredChats.firstIndex(where: { $0.chatInfo.chatType == .local }) {
|
||||
let privateNotes = filteredChats.remove(at: index)
|
||||
filteredChats.insert(privateNotes, at: 0)
|
||||
}
|
||||
return filteredChats
|
||||
}
|
||||
|
||||
private func filterChatSearched(_ chat: Chat, _ searchStr: String) -> Bool {
|
||||
let cInfo = chat.chatInfo
|
||||
return switch cInfo {
|
||||
case let .direct(contact):
|
||||
@@ -83,70 +96,42 @@ struct ChatItemForwardingView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func prohibitedByPref(_ chat: Chat) -> Bool {
|
||||
// preference checks should match checks in compose view
|
||||
let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
|
||||
let fileProhibited = (ci.content.msgContent?.isMediaOrFileAttachment ?? false) && !chat.groupFeatureEnabled(.files)
|
||||
let voiceProhibited = (ci.content.msgContent?.isVoice ?? false) && !chat.chatInfo.featureEnabled(.voice)
|
||||
return switch chat.chatInfo {
|
||||
case .direct: voiceProhibited
|
||||
case .group: simplexLinkProhibited || fileProhibited || voiceProhibited
|
||||
case .local: false
|
||||
private func canForwardToChat(_ chat: Chat) -> Bool {
|
||||
switch chat.chatInfo {
|
||||
case let .direct(contact): contact.sendMsgEnabled && !contact.nextSendGrpInv
|
||||
case let .group(groupInfo): groupInfo.sendMsgEnabled
|
||||
case let .local(noteFolder): noteFolder.sendMsgEnabled
|
||||
case .contactRequest: false
|
||||
case .contactConnection: false
|
||||
case .invalidJSON: false
|
||||
}
|
||||
}
|
||||
|
||||
private var hasSimplexLink: Bool {
|
||||
if let hasSimplexLink_ { return hasSimplexLink_ }
|
||||
let r =
|
||||
if let mcText = ci.content.msgContent?.text,
|
||||
let parsedMsg = parseSimpleXMarkdown(mcText) {
|
||||
parsedMsgHasSimplexLink(parsedMsg)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
hasSimplexLink_ = r
|
||||
return r
|
||||
}
|
||||
|
||||
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)
|
||||
@ViewBuilder private func forwardListNavLinkView(_ chat: Chat) -> some View {
|
||||
Button {
|
||||
if prohibited {
|
||||
alert = SomeAlert(
|
||||
alert: mkAlert(
|
||||
title: "Cannot forward message",
|
||||
message: "Selected chat preferences prohibit this message."
|
||||
),
|
||||
id: "forward prohibited by preferences"
|
||||
)
|
||||
dismiss()
|
||||
if chat.id == fromChatInfo.id {
|
||||
composeState = ComposeState(
|
||||
message: composeState.message,
|
||||
preview: composeState.linkPreview != nil ? composeState.preview : .noPreview,
|
||||
contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo)
|
||||
)
|
||||
} else {
|
||||
dismiss()
|
||||
if chat.id == fromChatInfo.id {
|
||||
composeState = ComposeState(
|
||||
message: composeState.message,
|
||||
preview: composeState.linkPreview != nil ? composeState.preview : .noPreview,
|
||||
contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo)
|
||||
)
|
||||
} else {
|
||||
composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo)
|
||||
chatModel.chatId = chat.id
|
||||
}
|
||||
composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo)
|
||||
chatModel.chatId = chat.id
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
ChatInfoImage(chat: chat, size: 30)
|
||||
.padding(.trailing, 2)
|
||||
Text(chat.chatInfo.chatViewName)
|
||||
.foregroundColor(prohibited ? theme.colors.secondary : theme.colors.onBackground)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
if chat.chatInfo.incognito {
|
||||
Spacer()
|
||||
@@ -154,7 +139,7 @@ struct ChatItemForwardingView: View {
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 22, height: 22)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -162,31 +147,10 @@ struct ChatItemForwardingView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func filterChatsToForwardTo() -> [Chat] {
|
||||
var filteredChats = ChatModel.shared.chats.filter { c in
|
||||
c.chatInfo.chatType != .local && canForwardToChat(c)
|
||||
}
|
||||
if let privateNotes = ChatModel.shared.chats.first(where: { $0.chatInfo.chatType == .local }) {
|
||||
filteredChats.insert(privateNotes, at: 0)
|
||||
}
|
||||
return filteredChats
|
||||
}
|
||||
|
||||
private func canForwardToChat(_ chat: Chat) -> Bool {
|
||||
switch chat.chatInfo {
|
||||
case let .direct(contact): contact.sendMsgEnabled && !contact.nextSendGrpInv
|
||||
case let .group(groupInfo): groupInfo.sendMsgEnabled
|
||||
case let .local(noteFolder): noteFolder.sendMsgEnabled
|
||||
case .contactRequest: false
|
||||
case .contactConnection: false
|
||||
case .invalidJSON: false
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ChatItemForwardingView(
|
||||
ci: ChatItem.getSample(1, .directSnd, .now, "hello"),
|
||||
fromChatInfo: .direct(contact: Contact.sampleData),
|
||||
composeState: Binding.constant(ComposeState(message: "hello"))
|
||||
).environmentObject(CurrentColors.toAppTheme())
|
||||
)
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ struct ChatItemInfoView: View {
|
||||
}
|
||||
else {
|
||||
Text("No history")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
@@ -232,7 +232,7 @@ struct ChatItemInfoView: View {
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(chatItemFrameColor(ci, theme))
|
||||
.modifier(ChatItemClipped())
|
||||
.cornerRadius(18)
|
||||
.contextMenu {
|
||||
if itemVersion.msgContent.text != "" {
|
||||
Button {
|
||||
@@ -262,19 +262,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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +301,7 @@ struct ChatItemInfoView: View {
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(quotedMsgFrameColor(qi, theme))
|
||||
.modifier(ChatItemClipped())
|
||||
.cornerRadius(18)
|
||||
.contextMenu {
|
||||
if qi.text != "" {
|
||||
Button {
|
||||
@@ -363,7 +362,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 +376,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 +414,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 +422,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 +437,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 +446,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(theme.colors.secondary, theme.colors.primary) {
|
||||
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,9 +11,9 @@ import SimpleXChat
|
||||
|
||||
struct ChatItemView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
var maxWidth: CGFloat = .infinity
|
||||
@State var scrollProxy: ScrollViewProxy? = nil
|
||||
@Binding var revealed: Bool
|
||||
@Binding var allowMenu: Bool
|
||||
@Binding var audioPlayer: AudioPlayer?
|
||||
@@ -24,6 +24,7 @@ struct ChatItemView: View {
|
||||
chatItem: ChatItem,
|
||||
showMember: Bool = false,
|
||||
maxWidth: CGFloat = .infinity,
|
||||
scrollProxy: ScrollViewProxy? = nil,
|
||||
revealed: Binding<Bool>,
|
||||
allowMenu: Binding<Bool> = .constant(false),
|
||||
audioPlayer: Binding<AudioPlayer?> = .constant(nil),
|
||||
@@ -33,6 +34,7 @@ struct ChatItemView: View {
|
||||
self.chat = chat
|
||||
self.chatItem = chatItem
|
||||
self.maxWidth = maxWidth
|
||||
_scrollProxy = .init(initialValue: scrollProxy)
|
||||
_revealed = revealed
|
||||
_allowMenu = allowMenu
|
||||
_audioPlayer = audioPlayer
|
||||
@@ -60,43 +62,12 @@ struct ChatItemView: View {
|
||||
}
|
||||
|
||||
private func framedItemView() -> some View {
|
||||
let preview = chatItem.content.msgContent
|
||||
.flatMap {
|
||||
switch $0 {
|
||||
case let .image(_, image): image
|
||||
case let .video(_, image, _): image
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
.map { dropImagePrefix($0) }
|
||||
.flatMap { Data(base64Encoded: $0) }
|
||||
.flatMap { UIImage(data: $0) }
|
||||
let adjustedMaxWidth = {
|
||||
if let preview, preview.size.width <= preview.size.height {
|
||||
maxWidth * 0.75
|
||||
} else {
|
||||
maxWidth
|
||||
}
|
||||
}()
|
||||
return FramedItemView(
|
||||
chat: chat,
|
||||
chatItem: chatItem,
|
||||
preview: preview,
|
||||
revealed: $revealed,
|
||||
maxWidth: maxWidth,
|
||||
imgWidth: adjustedMaxWidth,
|
||||
videoWidth: adjustedMaxWidth,
|
||||
allowMenu: $allowMenu,
|
||||
audioPlayer: $audioPlayer,
|
||||
playbackState: $playbackState,
|
||||
playbackTime: $playbackTime
|
||||
)
|
||||
FramedItemView(chat: chat, chatItem: chatItem, revealed: $revealed, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
|
||||
}
|
||||
}
|
||||
|
||||
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 +97,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 +131,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 +179,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 +187,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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -287,7 +287,6 @@ struct ComposeView: View {
|
||||
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
|
||||
ContextInvitingContactMemberView()
|
||||
}
|
||||
// preference checks should match checks in forwarding list
|
||||
let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
|
||||
let fileProhibited = composeState.attachmentPreview && !chat.groupFeatureEnabled(.files)
|
||||
let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice)
|
||||
@@ -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 {
|
||||
@@ -365,7 +363,7 @@ struct ComposeView: View {
|
||||
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)
|
||||
@@ -1068,7 +1066,7 @@ struct ComposeView: View {
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
let simplexLink = parsedMsgHasSimplexLink(parsedMsg)
|
||||
let simplexLink = parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false })
|
||||
return (url, simplexLink)
|
||||
}
|
||||
|
||||
@@ -1108,10 +1106,6 @@ struct ComposeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func parsedMsgHasSimplexLink(_ parsedMsg: [FormattedText]) -> Bool {
|
||||
parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false })
|
||||
}
|
||||
|
||||
struct ComposeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
|
||||
|
||||
@@ -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,7 +158,6 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ struct ContextInvitingContactMemberView: View {
|
||||
var body: some View {
|
||||
HStack {
|
||||
Image(systemName: "message")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
Text("Send direct message to connect")
|
||||
}
|
||||
.padding(12)
|
||||
|
||||
@@ -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,7 +40,6 @@ struct ContextItemView: View {
|
||||
} label: {
|
||||
Image(systemName: "multiply")
|
||||
}
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
.padding(12)
|
||||
.frame(minHeight: 50)
|
||||
@@ -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 {
|
||||
|
||||
@@ -50,7 +50,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)
|
||||
@@ -311,7 +311,7 @@ struct SendMessageView: View {
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "mic")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.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 {
|
||||
|
||||
@@ -71,7 +71,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 +91,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 +126,7 @@ struct AddGroupMembersViewCommon: View {
|
||||
.onChange(of: selectedContacts) { _ in
|
||||
searchFocussed = false
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func inviteMembersButton() -> some View {
|
||||
@@ -176,14 +174,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
|
||||
} else {
|
||||
icon = "circle"
|
||||
iconColor = Color(uiColor: .tertiaryLabel).asAnotherColorFromSecondary(theme)
|
||||
iconColor = Color(uiColor: .tertiaryLabel)
|
||||
}
|
||||
}
|
||||
return Button {
|
||||
@@ -201,7 +199,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 +209,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 +218,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 +230,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,13 @@ 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))
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
@@ -207,7 +200,6 @@ struct GroupChatInfoView: View {
|
||||
let groupMembers = await apiListMembers(groupInfo.groupId)
|
||||
await MainActor.run {
|
||||
chatModel.groupMembers = groupMembers.map { GMember.init($0) }
|
||||
chatModel.populateGroupMembersIndexes()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,13 +222,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)
|
||||
@@ -266,25 +259,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,7 +278,7 @@ 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 {
|
||||
@@ -319,7 +302,7 @@ struct GroupChatInfoView: View {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -335,14 +318,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 {
|
||||
@@ -360,7 +335,7 @@ struct GroupChatInfoView: View {
|
||||
creatingGroup: false
|
||||
)
|
||||
.navigationBarTitle("Group link")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
if groupLink == nil {
|
||||
@@ -393,7 +368,7 @@ struct GroupChatInfoView: View {
|
||||
welcomeText: groupInfo.groupProfile.description ?? ""
|
||||
)
|
||||
.navigationTitle("Welcome message")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
groupInfo.groupProfile.description == nil
|
||||
@@ -548,7 +523,7 @@ func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bo
|
||||
creatingGroup: creatingGroup
|
||||
)
|
||||
.navigationBarTitle("Group preferences")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
if creatingGroup {
|
||||
@@ -559,6 +534,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,7 @@ struct GroupLinkView: View {
|
||||
shouldCreate = false
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
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,7 @@ struct GroupMemberInfoView: View {
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
func connectViaAddressButton(_ contactLink: String) -> some View {
|
||||
@@ -328,7 +327,7 @@ struct GroupMemberInfoView: View {
|
||||
if mem.verified {
|
||||
(
|
||||
Text(Image(systemName: "checkmark.shield"))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.title2)
|
||||
+ Text(" ")
|
||||
+ Text(mem.displayName)
|
||||
@@ -426,7 +425,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)
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
//
|
||||
// ReverseList.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Levitating Pineapple on 11/06/2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
/// A List, which displays it's items in reverse order - from bottom to top
|
||||
struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIViewControllerRepresentable {
|
||||
|
||||
let items: Array<Item>
|
||||
|
||||
@Binding var scrollState: ReverseListScrollModel<Item>.State
|
||||
|
||||
/// Closure, that returns user interface for a given item
|
||||
let content: (Item) -> Content
|
||||
|
||||
let loadPage: () -> Void
|
||||
|
||||
func makeUIViewController(context: Context) -> Controller {
|
||||
Controller(representer: self)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ controller: Controller, context: Context) {
|
||||
if case let .scrollingTo(destination) = scrollState, !items.isEmpty {
|
||||
switch destination {
|
||||
case .nextPage:
|
||||
controller.scrollToNextPage()
|
||||
case let .item(id):
|
||||
controller.scroll(to: items.firstIndex(where: { $0.id == id }), position: .bottom)
|
||||
case .bottom:
|
||||
controller.scroll(to: .zero, position: .top)
|
||||
}
|
||||
} else {
|
||||
controller.update(items: items)
|
||||
}
|
||||
}
|
||||
|
||||
/// Controller, which hosts SwiftUI cells
|
||||
class Controller: UITableViewController {
|
||||
private enum Section { case main }
|
||||
private let representer: ReverseList
|
||||
private var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||
private var itemCount: Int = .zero
|
||||
private var bag = Set<AnyCancellable>()
|
||||
|
||||
init(representer: ReverseList) {
|
||||
self.representer = representer
|
||||
super.init(style: .plain)
|
||||
|
||||
// 1. Style
|
||||
tableView.separatorStyle = .none
|
||||
tableView.transform = .verticalFlip
|
||||
tableView.backgroundColor = .clear
|
||||
|
||||
// 2. Register cells
|
||||
if #available(iOS 16.0, *) {
|
||||
tableView.register(
|
||||
UITableViewCell.self,
|
||||
forCellReuseIdentifier: cellReuseId
|
||||
)
|
||||
} else {
|
||||
tableView.register(
|
||||
HostingCell<Content>.self,
|
||||
forCellReuseIdentifier: cellReuseId
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Configure data source
|
||||
self.dataSource = UITableViewDiffableDataSource<Section, Item>(
|
||||
tableView: tableView
|
||||
) { (tableView, indexPath, item) -> UITableViewCell? in
|
||||
if indexPath.item > self.itemCount - 8, self.itemCount > 8 {
|
||||
self.representer.loadPage()
|
||||
}
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath)
|
||||
if #available(iOS 16.0, *) {
|
||||
cell.contentConfiguration = UIHostingConfiguration { self.representer.content(item) }
|
||||
.margins(.all, .zero)
|
||||
.minSize(height: 1) // Passing zero will result in system default of 44 points being used
|
||||
} else {
|
||||
if let cell = cell as? HostingCell<Content> {
|
||||
cell.set(content: self.representer.content(item), parent: self)
|
||||
} else {
|
||||
fatalError("Unexpected Cell Type for: \(item)")
|
||||
}
|
||||
}
|
||||
cell.transform = .verticalFlip
|
||||
cell.selectionStyle = .none
|
||||
cell.backgroundColor = .clear
|
||||
return cell
|
||||
}
|
||||
|
||||
// 4. External state changes will require manual layout updates
|
||||
NotificationCenter.default
|
||||
.addObserver(
|
||||
self,
|
||||
selector: #selector(updateLayout),
|
||||
name: notificationName,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
deinit { NotificationCenter.default.removeObserver(self) }
|
||||
|
||||
@objc private func updateLayout() {
|
||||
if #available(iOS 16.0, *) {
|
||||
tableView.setNeedsLayout()
|
||||
tableView.layoutIfNeeded()
|
||||
} else {
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
/// Hides keyboard, when user begins to scroll.
|
||||
/// Equivalent to `.scrollDismissesKeyboard(.immediately)`
|
||||
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
UIApplication.shared
|
||||
.sendAction(
|
||||
#selector(UIResponder.resignFirstResponder),
|
||||
to: nil,
|
||||
from: nil,
|
||||
for: nil
|
||||
)
|
||||
}
|
||||
|
||||
/// Scrolls up
|
||||
func scrollToNextPage() {
|
||||
tableView.setContentOffset(
|
||||
CGPoint(
|
||||
x: tableView.contentOffset.x,
|
||||
y: tableView.contentOffset.y + tableView.bounds.height
|
||||
),
|
||||
animated: true
|
||||
)
|
||||
Task { representer.scrollState = .atDestination }
|
||||
}
|
||||
|
||||
/// Scrolls to Item at index path
|
||||
/// - Parameter indexPath: Item to scroll to - will scroll to beginning of the list, if `nil`
|
||||
func scroll(to index: Int?, position: UITableView.ScrollPosition) {
|
||||
if let index {
|
||||
var animated = false
|
||||
if #available(iOS 16.0, *) {
|
||||
animated = true
|
||||
}
|
||||
tableView.scrollToRow(
|
||||
at: IndexPath(row: index, section: .zero),
|
||||
at: position,
|
||||
animated: animated
|
||||
)
|
||||
Task { representer.scrollState = .atDestination }
|
||||
}
|
||||
}
|
||||
|
||||
func update(items: Array<Item>) {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(items)
|
||||
dataSource.defaultRowAnimation = .none
|
||||
dataSource.apply(
|
||||
snapshot,
|
||||
animatingDifferences: itemCount != .zero && abs(items.count - itemCount) == 1
|
||||
)
|
||||
itemCount = items.count
|
||||
}
|
||||
}
|
||||
|
||||
/// `UIHostingConfiguration` back-port for iOS14 and iOS15
|
||||
/// Implemented as a `UITableViewCell` that wraps and manages a generic `UIHostingController`
|
||||
private final class HostingCell<Hosted: View>: UITableViewCell {
|
||||
private let hostingController = UIHostingController<Hosted?>(rootView: nil)
|
||||
|
||||
/// 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()
|
||||
if hostingController.parent != parent { parent.addChild(hostingController) }
|
||||
if !contentView.subviews.contains(hostingController.view) {
|
||||
contentView.addSubview(hostingController.view)
|
||||
hostingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
hostingView.leadingAnchor
|
||||
.constraint(equalTo: contentView.leadingAnchor),
|
||||
hostingView.trailingAnchor
|
||||
.constraint(equalTo: contentView.trailingAnchor),
|
||||
hostingView.topAnchor
|
||||
.constraint(equalTo: contentView.topAnchor),
|
||||
hostingView.bottomAnchor
|
||||
.constraint(equalTo: contentView.bottomAnchor)
|
||||
])
|
||||
}
|
||||
if hostingController.parent != parent { hostingController.didMove(toParent: parent) }
|
||||
} else {
|
||||
fatalError("Hosting View not loaded \(hostingController)")
|
||||
}
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
hostingController.rootView = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages ``ReverseList`` scrolling
|
||||
class ReverseListScrollModel<Item: Identifiable>: ObservableObject {
|
||||
/// Represents Scroll State of ``ReverseList``
|
||||
enum State: Equatable {
|
||||
enum Destination: Equatable {
|
||||
case nextPage
|
||||
case item(Item.ID)
|
||||
case bottom
|
||||
}
|
||||
|
||||
case scrollingTo(Destination)
|
||||
case atDestination
|
||||
}
|
||||
|
||||
@Published var state: State = .atDestination
|
||||
|
||||
func scrollToNextPage() {
|
||||
state = .scrollingTo(.nextPage)
|
||||
}
|
||||
|
||||
func scrollToBottom() {
|
||||
state = .scrollingTo(.bottom)
|
||||
}
|
||||
|
||||
func scrollToItem(id: Item.ID) {
|
||||
state = .scrollingTo(.item(id))
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate let cellReuseId = "hostingCell"
|
||||
|
||||
fileprivate let notificationName = NSNotification.Name(rawValue: "reverseListNeedsLayout")
|
||||
|
||||
fileprivate extension CGAffineTransform {
|
||||
/// Transform that vertically flips the view, preserving it's location
|
||||
static let verticalFlip = CGAffineTransform(scaleX: 1, y: -1)
|
||||
}
|
||||
|
||||
extension NotificationCenter {
|
||||
static func postReverseListNeedsLayout() {
|
||||
NotificationCenter.default.post(
|
||||
name: notificationName,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Disable animation on iOS 15
|
||||
func withConditionalAnimation<Result>(
|
||||
_ animation: Animation? = .default,
|
||||
_ body: () throws -> Result
|
||||
) rethrows -> Result {
|
||||
if #available(iOS 16.0, *) {
|
||||
try withAnimation(animation, body)
|
||||
} else {
|
||||
try body()
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
@@ -124,6 +123,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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,7 +355,7 @@ 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))
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,9 @@ struct ChatListView: View {
|
||||
HStack(spacing: 4) {
|
||||
Text("Chats")
|
||||
.font(.headline)
|
||||
SubsStatusIndicator()
|
||||
if chatModel.chats.count > 0 {
|
||||
toggleFilterButton()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
@@ -131,6 +133,15 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleFilterButton() -> some View {
|
||||
Button {
|
||||
showUnreadAndFavorites = !showUnreadAndFavorites
|
||||
} label: {
|
||||
Image(systemName: "line.3.horizontal.decrease.circle" + (showUnreadAndFavorites ? ".fill" : ""))
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private var chatList: some View {
|
||||
let cs = filteredChats()
|
||||
ZStack {
|
||||
@@ -164,7 +175,7 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
if cs.isEmpty && !chatModel.chats.isEmpty {
|
||||
Text("No filtered chats").foregroundColor(theme.colors.secondary)
|
||||
Text("No filtered chats").foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,7 +204,7 @@ struct ChatListView: View {
|
||||
|
||||
Spacer()
|
||||
Text("You have no chats")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding(.trailing, 6)
|
||||
@@ -213,7 +224,9 @@ struct ChatListView: View {
|
||||
|
||||
@ViewBuilder private func chatView() -> some View {
|
||||
if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) {
|
||||
ChatView(chat: chat)
|
||||
ChatView(chat: chat).onAppear {
|
||||
loadChat(chat: chat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,75 +278,6 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct SubsStatusIndicator: View {
|
||||
@State private var subs: SMPServerSubs = SMPServerSubs.newSMPServerSubs
|
||||
@State private var sess: ServerSessions = ServerSessions.newServerSessions
|
||||
@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
|
||||
let initialInterval: TimeInterval = 1.0
|
||||
let regularInterval: TimeInterval = 3.0
|
||||
let initialPhaseDuration: TimeInterval = 10.0 // Duration for initial phase in seconds
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
showServersSummary = true
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
SubscriptionStatusIndicatorView(subs: subs, sess: sess)
|
||||
if showSubscriptionPercentage {
|
||||
SubscriptionStatusPercentageView(subs: subs, sess: sess)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
startInitialTimer()
|
||||
}
|
||||
.onDisappear {
|
||||
stopTimer()
|
||||
}
|
||||
.sheet(isPresented: $showServersSummary) {
|
||||
ServersSummaryView()
|
||||
}
|
||||
}
|
||||
|
||||
private func startInitialTimer() {
|
||||
timer = Timer.scheduledTimer(withTimeInterval: initialInterval, repeats: true) { _ in
|
||||
getServersSummary()
|
||||
timerCounter += 1
|
||||
// Switch to the regular timer after the initial phase
|
||||
if timerCounter * Int(initialInterval) >= Int(initialPhaseDuration) {
|
||||
switchToRegularTimer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func switchToRegularTimer() {
|
||||
timer?.invalidate()
|
||||
timer = Timer.scheduledTimer(withTimeInterval: regularInterval, repeats: true) { _ in
|
||||
getServersSummary()
|
||||
}
|
||||
}
|
||||
|
||||
func stopTimer() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
}
|
||||
|
||||
private func getServersSummary() {
|
||||
do {
|
||||
let summ = try getAgentServersSummary()
|
||||
(subs, sess) = (summ.allUsersSMP.smpTotals.subs, summ.allUsersSMP.smpTotals.sessions)
|
||||
} catch let error {
|
||||
logger.error("getAgentServersSummary error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatListSearchBar: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@@ -343,9 +287,9 @@ struct ChatListSearchBar: View {
|
||||
@Binding var searchShowingSimplexLink: Bool
|
||||
@Binding var searchChatFilteredBySimplexLink: String?
|
||||
@State private var ignoreSearchTextChange = false
|
||||
@State private var showScanCodeSheet = false
|
||||
@State private var alert: PlanAndConnectAlert?
|
||||
@State private var sheet: PlanAndConnectActionSheet?
|
||||
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
@@ -353,7 +297,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)
|
||||
@@ -362,10 +306,30 @@ struct ChatListSearchBar: View {
|
||||
.onTapGesture {
|
||||
searchText = ""
|
||||
}
|
||||
} else if !searchFocussed {
|
||||
HStack(spacing: 24) {
|
||||
if m.pasteboardHasStrings {
|
||||
Image(systemName: "doc")
|
||||
.onTapGesture {
|
||||
if let str = UIPasteboard.general.string {
|
||||
searchText = str
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image(systemName: "qrcode")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.onTapGesture {
|
||||
showScanCodeSheet = true
|
||||
}
|
||||
}
|
||||
.padding(.trailing, 2)
|
||||
}
|
||||
}
|
||||
.padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.background(Color(.tertiarySystemFill))
|
||||
.cornerRadius(10.0)
|
||||
|
||||
@@ -376,12 +340,14 @@ struct ChatListSearchBar: View {
|
||||
searchText = ""
|
||||
searchFocussed = false
|
||||
}
|
||||
} else if m.chats.count > 0 {
|
||||
toggleFilterButton()
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
.sheet(isPresented: $showScanCodeSheet) {
|
||||
NewChatView(selection: .connect, showQRCodeScanner: true)
|
||||
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil) // fixes .refreshable in ChatListView affecting nested view
|
||||
}
|
||||
.onChange(of: searchFocussed) { sf in
|
||||
withAnimation { searchMode = sf }
|
||||
}
|
||||
@@ -415,21 +381,6 @@ struct ChatListSearchBar: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleFilterButton() -> some View {
|
||||
ZStack {
|
||||
Color.clear
|
||||
.frame(width: 22, height: 22)
|
||||
Image(systemName: showUnreadAndFavorites ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.foregroundColor(showUnreadAndFavorites ? theme.colors.primary : theme.colors.secondary)
|
||||
.frame(width: showUnreadAndFavorites ? 22 : 16, height: showUnreadAndFavorites ? 22 : 16)
|
||||
.onTapGesture {
|
||||
showUnreadAndFavorites = !showUnreadAndFavorites
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func connect(_ link: String) {
|
||||
planAndConnect(
|
||||
link,
|
||||
|
||||
@@ -15,6 +15,7 @@ struct ChatPreviewView: View {
|
||||
@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 +37,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 +95,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 : theme.colors.primary)
|
||||
case .memAccepted: v.foregroundColor(.secondary)
|
||||
default: if deleting { v.foregroundColor(.secondary) } else { v }
|
||||
}
|
||||
default: previewTitle(t)
|
||||
}
|
||||
@@ -108,7 +109,7 @@ struct ChatPreviewView: View {
|
||||
|
||||
private var verifiedIcon: Text {
|
||||
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.baselineOffset(1)
|
||||
.kerning(-2)
|
||||
}
|
||||
@@ -137,7 +138,7 @@ struct ChatPreviewView: View {
|
||||
.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()
|
||||
@@ -153,7 +154,7 @@ struct ChatPreviewView: View {
|
||||
let msg = draft.message
|
||||
return image("rectangle.and.pencil.and.ellipsis", color: theme.colors.primary)
|
||||
+ 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 +173,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
|
||||
@@ -257,38 +258,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()
|
||||
}
|
||||
|
||||
@@ -49,7 +49,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 +64,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 +83,7 @@ struct ContactConnectionInfo: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
if #available(iOS 16, *) {
|
||||
v
|
||||
} else {
|
||||
@@ -167,24 +166,24 @@ 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)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ struct ContactRequestView: View {
|
||||
.padding(.trailing, 8)
|
||||
.padding(.top, 4)
|
||||
.frame(minWidth: 60, alignment: .trailing)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.bottom, 2)
|
||||
|
||||
|
||||
@@ -1,738 +0,0 @@
|
||||
//
|
||||
// ServersSummaryView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 25.06.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ServersSummaryView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@State private var serversSummary: PresentedServersSummary? = nil
|
||||
@State private var selectedUserCategory: PresentedUserCategory = .allUsers
|
||||
@State private var selectedServerType: PresentedServerType = .smp
|
||||
@State private var selectedSMPServer: String? = nil
|
||||
@State private var selectedXFTPServer: String? = nil
|
||||
@State private var timer: Timer? = nil
|
||||
@State private var alert: SomeAlert?
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false
|
||||
|
||||
enum PresentedUserCategory {
|
||||
case currentUser
|
||||
case allUsers
|
||||
}
|
||||
|
||||
enum PresentedServerType {
|
||||
case smp
|
||||
case xftp
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
viewBody()
|
||||
.navigationTitle("Servers info")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
shareButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if m.users.filter({ u in u.user.activeUser || !u.user.hidden }).count == 1 {
|
||||
selectedUserCategory = .currentUser
|
||||
}
|
||||
getServersSummary()
|
||||
startTimer()
|
||||
}
|
||||
.onDisappear {
|
||||
stopTimer()
|
||||
}
|
||||
.alert(item: $alert) { $0.alert }
|
||||
}
|
||||
|
||||
private func startTimer() {
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
||||
getServersSummary()
|
||||
}
|
||||
}
|
||||
|
||||
func stopTimer() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
}
|
||||
|
||||
private func shareButton() -> some View {
|
||||
Button {
|
||||
if let serversSummary = serversSummary {
|
||||
showShareSheet(items: [encodePrettyPrinted(serversSummary)])
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
}
|
||||
.disabled(serversSummary == nil)
|
||||
}
|
||||
|
||||
public func encodePrettyPrinted<T: Encodable>(_ value: T) -> String {
|
||||
let encoder = jsonEncoder
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
let data = try! encoder.encode(value)
|
||||
return String(decoding: data, as: UTF8.self)
|
||||
}
|
||||
|
||||
@ViewBuilder private func viewBody() -> some View {
|
||||
if let summ = serversSummary {
|
||||
List {
|
||||
Group {
|
||||
if m.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1 {
|
||||
Picker("User selection", selection: $selectedUserCategory) {
|
||||
Text("All users").tag(PresentedUserCategory.allUsers)
|
||||
Text("Current user").tag(PresentedUserCategory.currentUser)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
|
||||
Picker("Server type", selection: $selectedServerType) {
|
||||
Text("Messages").tag(PresentedServerType.smp)
|
||||
Text("Files").tag(PresentedServerType.xftp)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
|
||||
switch (selectedUserCategory, selectedServerType) {
|
||||
case (.allUsers, .smp):
|
||||
let smpSumm = summ.allUsersSMP
|
||||
let (totals, curr, prev, prox) = (smpSumm.smpTotals, smpSumm.currentlyUsedSMPServers, smpSumm.previouslyUsedSMPServers, smpSumm.onlyProxiedSMPServers)
|
||||
|
||||
SMPStatsView(stats: totals.stats, statsStartedAt: summ.statsStartedAt)
|
||||
|
||||
smpSubsSection(totals)
|
||||
|
||||
if curr.count > 0 {
|
||||
smpServersListView(curr, summ.statsStartedAt, "Connected servers")
|
||||
}
|
||||
if prev.count > 0 {
|
||||
smpServersListView(prev, summ.statsStartedAt, "Previously connected servers")
|
||||
}
|
||||
if prox.count > 0 {
|
||||
smpServersListView(prox, summ.statsStartedAt, "Proxied servers", "You are not connected to these servers. Private routing is used to deliver messages to them.")
|
||||
}
|
||||
|
||||
ServerSessionsView(sess: totals.sessions)
|
||||
case (.currentUser, .smp):
|
||||
let smpSumm = summ.currentUserSMP
|
||||
let (totals, curr, prev, prox) = (smpSumm.smpTotals, smpSumm.currentlyUsedSMPServers, smpSumm.previouslyUsedSMPServers, smpSumm.onlyProxiedSMPServers)
|
||||
|
||||
SMPStatsView(stats: totals.stats, statsStartedAt: summ.statsStartedAt)
|
||||
|
||||
smpSubsSection(totals)
|
||||
|
||||
if curr.count > 0 {
|
||||
smpServersListView(curr, summ.statsStartedAt, "Connected servers")
|
||||
}
|
||||
if prev.count > 0 {
|
||||
smpServersListView(prev, summ.statsStartedAt, "Previously connected servers")
|
||||
}
|
||||
if prox.count > 0 {
|
||||
smpServersListView(prox, summ.statsStartedAt, "Proxied servers", "You are not connected to these servers. Private routing is used to deliver messages to them.")
|
||||
}
|
||||
|
||||
ServerSessionsView(sess: totals.sessions)
|
||||
case (.allUsers, .xftp):
|
||||
let xftpSumm = summ.allUsersXFTP
|
||||
let (totals, curr, prev) = (xftpSumm.xftpTotals, xftpSumm.currentlyUsedXFTPServers, xftpSumm.previouslyUsedXFTPServers)
|
||||
|
||||
XFTPStatsView(stats: totals.stats, statsStartedAt: summ.statsStartedAt)
|
||||
|
||||
if curr.count > 0 {
|
||||
xftpServersListView(curr, summ.statsStartedAt, "Connected servers")
|
||||
}
|
||||
if prev.count > 0 {
|
||||
xftpServersListView(prev, summ.statsStartedAt, "Previously connected servers")
|
||||
}
|
||||
|
||||
ServerSessionsView(sess: totals.sessions)
|
||||
case (.currentUser, .xftp):
|
||||
let xftpSumm = summ.currentUserXFTP
|
||||
let (totals, curr, prev) = (xftpSumm.xftpTotals, xftpSumm.currentlyUsedXFTPServers, xftpSumm.previouslyUsedXFTPServers)
|
||||
|
||||
XFTPStatsView(stats: totals.stats, statsStartedAt: summ.statsStartedAt)
|
||||
|
||||
if curr.count > 0 {
|
||||
xftpServersListView(curr, summ.statsStartedAt, "Connected servers")
|
||||
}
|
||||
if prev.count > 0 {
|
||||
xftpServersListView(prev, summ.statsStartedAt, "Previously connected servers")
|
||||
}
|
||||
|
||||
ServerSessionsView(sess: totals.sessions)
|
||||
}
|
||||
|
||||
Section {
|
||||
reconnectAllButton()
|
||||
resetStatsButton()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("No info, try to reload")
|
||||
}
|
||||
}
|
||||
|
||||
private func smpSubsSection(_ totals: SMPTotals) -> some View {
|
||||
Section {
|
||||
infoRow("Connections subscribed", numOrDash(totals.subs.ssActive))
|
||||
infoRow("Total", numOrDash(totals.subs.total))
|
||||
} header: {
|
||||
HStack {
|
||||
Text("Message subscriptions")
|
||||
SubscriptionStatusIndicatorView(subs: totals.subs, sess: totals.sessions)
|
||||
if showSubscriptionPercentage {
|
||||
SubscriptionStatusPercentageView(subs: totals.subs, sess: totals.sessions)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func reconnectAllButton() -> some View {
|
||||
Button {
|
||||
alert = SomeAlert(
|
||||
alert: Alert(
|
||||
title: Text("Reconnect all servers?"),
|
||||
message: Text("Reconnect all connected servers to force message delivery. It uses additional traffic."),
|
||||
primaryButton: .default(Text("Ok")) {
|
||||
Task {
|
||||
do {
|
||||
try await reconnectAllServers()
|
||||
} catch let error {
|
||||
alert = SomeAlert(
|
||||
alert: mkAlert(
|
||||
title: "Error reconnecting servers",
|
||||
message: "\(responseError(error))"
|
||||
),
|
||||
id: "error reconnecting servers"
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
),
|
||||
id: "reconnect servers question"
|
||||
)
|
||||
} label: {
|
||||
Text("Reconnect all servers")
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func smpServersListView(
|
||||
_ servers: [SMPServerSummary],
|
||||
_ statsStartedAt: Date,
|
||||
_ header: LocalizedStringKey? = nil,
|
||||
_ footer: LocalizedStringKey? = nil
|
||||
) -> some View {
|
||||
let sortedServers = servers.sorted {
|
||||
$0.hasSubs == $1.hasSubs
|
||||
? serverAddress($0.smpServer) < serverAddress($1.smpServer)
|
||||
: $0.hasSubs && !$1.hasSubs
|
||||
}
|
||||
Section {
|
||||
ForEach(sortedServers) { server in
|
||||
smpServerView(server, statsStartedAt)
|
||||
}
|
||||
} header: {
|
||||
if let header = header {
|
||||
Text(header)
|
||||
}
|
||||
} footer: {
|
||||
if let footer = footer {
|
||||
Text(footer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func smpServerView(_ srvSumm: SMPServerSummary, _ statsStartedAt: Date) -> some View {
|
||||
NavigationLink(tag: srvSumm.id, selection: $selectedSMPServer) {
|
||||
SMPServerSummaryView(
|
||||
summary: srvSumm,
|
||||
statsStartedAt: statsStartedAt
|
||||
)
|
||||
.navigationBarTitle("SMP server")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
HStack {
|
||||
Text(serverAddress(srvSumm.smpServer))
|
||||
.lineLimit(1)
|
||||
if let subs = srvSumm.subs {
|
||||
Spacer()
|
||||
if showSubscriptionPercentage {
|
||||
SubscriptionStatusPercentageView(subs: subs, sess: srvSumm.sessionsOrNew)
|
||||
}
|
||||
SubscriptionStatusIndicatorView(subs: subs, sess: srvSumm.sessionsOrNew)
|
||||
} else if let sess = srvSumm.sessions {
|
||||
Spacer()
|
||||
Image(systemName: "arrow.up.circle")
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(sessIconColor(sess), Color.clear)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func serverAddress(_ server: String) -> String {
|
||||
parseServerAddress(server)?.hostnames.first ?? server
|
||||
}
|
||||
|
||||
private func sessIconColor(_ sess: ServerSessions) -> Color {
|
||||
let online = m.networkInfo.online
|
||||
return (
|
||||
online && sess.ssConnected > 0
|
||||
? sessionActiveColor
|
||||
: Color(uiColor: .tertiaryLabel)
|
||||
)
|
||||
}
|
||||
|
||||
private var sessionActiveColor: Color {
|
||||
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||
return onionHosts == .require ? .indigo : .accentColor
|
||||
}
|
||||
|
||||
@ViewBuilder private func xftpServersListView(
|
||||
_ servers: [XFTPServerSummary],
|
||||
_ statsStartedAt: Date,
|
||||
_ header: LocalizedStringKey? = nil,
|
||||
_ footer: LocalizedStringKey? = nil
|
||||
) -> some View {
|
||||
let sortedServers = servers.sorted { serverAddress($0.xftpServer) < serverAddress($1.xftpServer) }
|
||||
Section {
|
||||
ForEach(sortedServers) { server in
|
||||
xftpServerView(server, statsStartedAt)
|
||||
}
|
||||
} header: {
|
||||
if let header = header {
|
||||
Text(header)
|
||||
}
|
||||
} footer: {
|
||||
if let footer = footer {
|
||||
Text(footer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func xftpServerView(_ srvSumm: XFTPServerSummary, _ statsStartedAt: Date) -> some View {
|
||||
NavigationLink(tag: srvSumm.id, selection: $selectedXFTPServer) {
|
||||
XFTPServerSummaryView(
|
||||
summary: srvSumm,
|
||||
statsStartedAt: statsStartedAt
|
||||
)
|
||||
.navigationBarTitle("XFTP server")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
HStack {
|
||||
Text(serverAddress(srvSumm.xftpServer))
|
||||
.lineLimit(1)
|
||||
if let inProgressIcon = inProgressIcon(srvSumm) {
|
||||
Spacer()
|
||||
Image(systemName: inProgressIcon)
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(sessionActiveColor, Color.clear)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func inProgressIcon(_ srvSumm: XFTPServerSummary) -> String? {
|
||||
switch (srvSumm.rcvInProgress, srvSumm.sndInProgress, srvSumm.delInProgress) {
|
||||
case (false, false, false): nil
|
||||
case (true, false, false): "arrow.down.circle"
|
||||
case (false, true, false): "arrow.up.circle"
|
||||
case (false, false, true): "trash.circle"
|
||||
default: "arrow.up.arrow.down.circle"
|
||||
}
|
||||
}
|
||||
|
||||
private func resetStatsButton() -> some View {
|
||||
Button {
|
||||
alert = SomeAlert(
|
||||
alert: Alert(
|
||||
title: Text("Reset all statistics?"),
|
||||
message: Text("Servers statistics will be reset - this cannot be undone!"),
|
||||
primaryButton: .destructive(Text("Reset")) {
|
||||
Task {
|
||||
do {
|
||||
try await resetAgentServersStats()
|
||||
getServersSummary()
|
||||
} catch let error {
|
||||
alert = SomeAlert(
|
||||
alert: mkAlert(
|
||||
title: "Error resetting statistics",
|
||||
message: "\(responseError(error))"
|
||||
),
|
||||
id: "error resetting statistics"
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
),
|
||||
id: "reset statistics question"
|
||||
)
|
||||
} label: {
|
||||
Text("Reset all statistics")
|
||||
}
|
||||
}
|
||||
|
||||
private func getServersSummary() {
|
||||
do {
|
||||
serversSummary = try getAgentServersSummary()
|
||||
} catch let error {
|
||||
logger.error("getAgentServersSummary error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SubscriptionStatusIndicatorView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
var subs: SMPServerSubs
|
||||
var sess: ServerSessions
|
||||
|
||||
var body: some View {
|
||||
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||
let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, sess)
|
||||
if #available(iOS 16.0, *) {
|
||||
Image(systemName: "dot.radiowaves.up.forward", variableValue: variableValue)
|
||||
.foregroundColor(color)
|
||||
} else {
|
||||
Image(systemName: "dot.radiowaves.up.forward")
|
||||
.foregroundColor(color.opacity(opacity))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SubscriptionStatusPercentageView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
var subs: SMPServerSubs
|
||||
var sess: ServerSessions
|
||||
|
||||
var body: some View {
|
||||
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||
let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, sess)
|
||||
Text(verbatim: "\(Int(floor(statusPercent * 100)))%")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
func subscriptionStatusColorAndPercentage(_ online: Bool, _ onionHosts: OnionHosts, _ subs: SMPServerSubs, _ sess: ServerSessions) -> (Color, Double, Double, Double) {
|
||||
func roundedToQuarter(_ n: Double) -> Double {
|
||||
n >= 1 ? 1
|
||||
: n <= 0 ? 0
|
||||
: (n * 4).rounded() / 4
|
||||
}
|
||||
|
||||
let activeColor: Color = onionHosts == .require ? .indigo : .accentColor
|
||||
let noConnColorAndPercent: (Color, Double, Double, Double) = (Color(uiColor: .tertiaryLabel), 1, 1, 0)
|
||||
let activeSubsRounded = roundedToQuarter(subs.shareOfActive)
|
||||
|
||||
return online && subs.total > 0
|
||||
? (
|
||||
subs.ssActive == 0
|
||||
? (
|
||||
sess.ssConnected == 0 ? noConnColorAndPercent : (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive)
|
||||
)
|
||||
: ( // ssActive > 0
|
||||
sess.ssConnected == 0
|
||||
? (.orange, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) // This would mean implementation error
|
||||
: (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive)
|
||||
)
|
||||
)
|
||||
: noConnColorAndPercent
|
||||
}
|
||||
|
||||
struct SMPServerSummaryView: View {
|
||||
var summary: SMPServerSummary
|
||||
var statsStartedAt: Date
|
||||
@State private var alert: SomeAlert?
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section("Server address") {
|
||||
Text(summary.smpServer)
|
||||
.textSelection(.enabled)
|
||||
if summary.known == true {
|
||||
NavigationLink {
|
||||
ProtocolServersView(serverProtocol: .smp)
|
||||
.navigationTitle("Your SMP servers")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text("Open server settings")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let stats = summary.stats {
|
||||
SMPStatsView(stats: stats, statsStartedAt: statsStartedAt)
|
||||
}
|
||||
|
||||
if let subs = summary.subs {
|
||||
smpSubsSection(subs)
|
||||
}
|
||||
|
||||
if let sess = summary.sessions {
|
||||
ServerSessionsView(sess: sess)
|
||||
}
|
||||
}
|
||||
.alert(item: $alert) { $0.alert }
|
||||
}
|
||||
|
||||
private func smpSubsSection(_ subs: SMPServerSubs) -> some View {
|
||||
Section {
|
||||
infoRow("Connections subscribed", numOrDash(subs.ssActive))
|
||||
infoRow("Pending", numOrDash(subs.ssPending))
|
||||
infoRow("Total", numOrDash(subs.total))
|
||||
reconnectButton()
|
||||
} header: {
|
||||
HStack {
|
||||
Text("Message subscriptions")
|
||||
SubscriptionStatusIndicatorView(subs: subs, sess: summary.sessionsOrNew)
|
||||
if showSubscriptionPercentage {
|
||||
SubscriptionStatusPercentageView(subs: subs, sess: summary.sessionsOrNew)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func reconnectButton() -> some View {
|
||||
Button {
|
||||
alert = SomeAlert(
|
||||
alert: Alert(
|
||||
title: Text("Reconnect server?"),
|
||||
message: Text("Reconnect server to force message delivery. It uses additional traffic."),
|
||||
primaryButton: .default(Text("Ok")) {
|
||||
Task {
|
||||
do {
|
||||
try await reconnectServer(smpServer: summary.smpServer)
|
||||
} catch let error {
|
||||
alert = SomeAlert(
|
||||
alert: mkAlert(
|
||||
title: "Error reconnecting server",
|
||||
message: "\(responseError(error))"
|
||||
),
|
||||
id: "error reconnecting server"
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
),
|
||||
id: "reconnect server question"
|
||||
)
|
||||
} label: {
|
||||
Text("Reconnect")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ServerSessionsView: View {
|
||||
var sess: ServerSessions
|
||||
|
||||
var body: some View {
|
||||
Section("Transport sessions") {
|
||||
infoRow("Connected", numOrDash(sess.ssConnected))
|
||||
infoRow("Errors", numOrDash(sess.ssErrors))
|
||||
infoRow("Connecting", numOrDash(sess.ssConnecting))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SMPStatsView: View {
|
||||
var stats: AgentSMPServerStatsData
|
||||
var statsStartedAt: Date
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
infoRow("Messages sent", numOrDash(stats._sentDirect + stats._sentViaProxy))
|
||||
infoRow("Messages received", numOrDash(stats._recvMsgs))
|
||||
NavigationLink {
|
||||
DetailedSMPStatsView(stats: stats, statsStartedAt: statsStartedAt)
|
||||
.navigationTitle("Detailed statistics")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text("Details")
|
||||
}
|
||||
} header: {
|
||||
Text("Statistics")
|
||||
} footer: {
|
||||
Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is private to your device.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func numOrDash(_ n: Int) -> String {
|
||||
n == 0 ? "-" : "\(n)"
|
||||
}
|
||||
|
||||
struct DetailedSMPStatsView: View {
|
||||
var stats: AgentSMPServerStatsData
|
||||
var statsStartedAt: Date
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section("Sent messages") {
|
||||
infoRow("Sent total", numOrDash(stats._sentDirect + stats._sentViaProxy))
|
||||
infoRowTwoValues("Sent directly", "attempts", stats._sentDirect, stats._sentDirectAttempts)
|
||||
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)
|
||||
}
|
||||
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)
|
||||
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)
|
||||
}
|
||||
Section {
|
||||
infoRow("Created", numOrDash(stats._connCreated))
|
||||
infoRow("Secured", numOrDash(stats._connCreated))
|
||||
infoRow("Completed", numOrDash(stats._connCompleted))
|
||||
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 errors", numOrDash(stats._connSubErrs))
|
||||
} header: {
|
||||
Text("Connections")
|
||||
} footer: {
|
||||
Text("Starting from \(localTimestamp(statsStartedAt)).")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func infoRowTwoValues(_ title: LocalizedStringKey, _ title2: LocalizedStringKey, _ value: Int, _ value2: Int) -> some View {
|
||||
HStack {
|
||||
Text(title) + Text(verbatim: " / ").font(.caption2) + Text(title2).font(.caption2)
|
||||
Spacer()
|
||||
Group {
|
||||
if value == 0 && value2 == 0 {
|
||||
Text(verbatim: "-")
|
||||
} else {
|
||||
Text(numOrDash(value)) + Text(verbatim: " / ").font(.caption2) + Text(numOrDash(value2)).font(.caption2)
|
||||
}
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
struct XFTPServerSummaryView: View {
|
||||
var summary: XFTPServerSummary
|
||||
var statsStartedAt: Date
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section("Server address") {
|
||||
Text(summary.xftpServer)
|
||||
.textSelection(.enabled)
|
||||
if summary.known == true {
|
||||
NavigationLink {
|
||||
ProtocolServersView(serverProtocol: .xftp)
|
||||
.navigationTitle("Your XFTP servers")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text("Open server settings")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let stats = summary.stats {
|
||||
XFTPStatsView(stats: stats, statsStartedAt: statsStartedAt)
|
||||
}
|
||||
|
||||
if let sess = summary.sessions {
|
||||
ServerSessionsView(sess: sess)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct XFTPStatsView: View {
|
||||
var stats: AgentXFTPServerStatsData
|
||||
var statsStartedAt: Date
|
||||
@State private var expanded = false
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
infoRow("Uploaded", prettySize(stats._uploadsSize))
|
||||
infoRow("Downloaded", prettySize(stats._downloadsSize))
|
||||
NavigationLink {
|
||||
DetailedXFTPStatsView(stats: stats, statsStartedAt: statsStartedAt)
|
||||
.navigationTitle("Detailed statistics")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text("Details")
|
||||
}
|
||||
} header: {
|
||||
Text("Statistics")
|
||||
} footer: {
|
||||
Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is private to your device.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func prettySize(_ sizeInKB: Int64) -> String {
|
||||
let kb: Int64 = 1024
|
||||
return sizeInKB == 0 ? "-" : ByteCountFormatter.string(fromByteCount: sizeInKB * kb, countStyle: .binary)
|
||||
}
|
||||
|
||||
struct DetailedXFTPStatsView: View {
|
||||
var stats: AgentXFTPServerStatsData
|
||||
var statsStartedAt: Date
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section("Uploaded files") {
|
||||
infoRow("Size", prettySize(stats._uploadsSize))
|
||||
infoRowTwoValues("Chunks uploaded", "attempts", stats._uploads, stats._uploadAttempts)
|
||||
infoRow("Upload errors", numOrDash(stats._uploadErrs))
|
||||
infoRowTwoValues("Chunks deleted", "attempts", stats._deletions, stats._deleteAttempts)
|
||||
infoRow("Deletion errors", numOrDash(stats._deleteErrs))
|
||||
}
|
||||
Section {
|
||||
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)
|
||||
} header: {
|
||||
Text("Downloaded files")
|
||||
} footer: {
|
||||
Text("Starting from \(localTimestamp(statsStartedAt)).")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ServersSummaryView()
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import SimpleXChat
|
||||
|
||||
struct UserPicker: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var showSettings: Bool
|
||||
@@ -125,7 +126,7 @@ 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 {
|
||||
@@ -150,7 +151,7 @@ struct UserPicker: View {
|
||||
Spacer()
|
||||
Image(systemName: icon)
|
||||
.symbolRenderingMode(.monochrome)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 22)
|
||||
|
||||
@@ -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,25 @@ 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))
|
||||
.modifier(ThemedBackground())
|
||||
} 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 +132,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 +141,35 @@ 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))
|
||||
.modifier(ThemedBackground())
|
||||
} 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 +184,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ struct ChatViewBackground: ViewModifier {
|
||||
content.background(
|
||||
Canvas { context, size in
|
||||
var image = context.resolve(image)
|
||||
image.shading = .color(tint)
|
||||
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
|
||||
let scale = imageScale
|
||||
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)
|
||||
@@ -34,49 +34,47 @@ struct ChatViewBackground: ViewModifier {
|
||||
}
|
||||
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):
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
// let scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height))
|
||||
// let scaledWidth = (image.width * scale.scaleX).roundToInt()
|
||||
// let scaledHeight = (image.height * scale.scaleY).roundToInt()
|
||||
// drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
// if (scaleType == WallpaperScaleType.FIT) {
|
||||
// if (scaledWidth < size.width) {
|
||||
// // has black lines at left and right sides
|
||||
// var x = (size.width - scaledWidth) / 2
|
||||
// while (x > 0) {
|
||||
// drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
// x -= scaledWidth
|
||||
// }
|
||||
// x = size.width - (size.width - scaledWidth) / 2
|
||||
// while (x < size.width) {
|
||||
// drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
// x += scaledWidth
|
||||
// }
|
||||
// } else {
|
||||
// // has black lines at top and bottom sides
|
||||
// var y = (size.height - scaledHeight) / 2
|
||||
// while (y > 0) {
|
||||
// drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
// y -= scaledHeight
|
||||
// }
|
||||
// y = size.height - (size.height - scaledHeight) / 2
|
||||
// while (y < size.height) {
|
||||
// drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
// y += scaledHeight
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
context.fill(Path(rect), with: .color(tint))
|
||||
}
|
||||
case WallpaperType.empty: ()
|
||||
case WallpaperType.Empty: ()
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -85,18 +83,9 @@ struct ChatViewBackground: ViewModifier {
|
||||
|
||||
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(
|
||||
WallpaperType.Preset(
|
||||
filename,
|
||||
scale
|
||||
scale ?? themeOverridesDefault.get().first { $0.wallpaper != nil && $0.wallpaper!.preset == filename && $0.base == base }?.wallpaper?.scale ?? 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// ContextMenu2.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 09/08/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
func uiKitContextMenu(hasImageOrVideo: Bool, maxWidth: CGFloat, itemWidth: Binding<CGFloat>, menu: Binding<UIMenu>, allowMenu: Binding<Bool>, backgroundColor: Color) -> some View {
|
||||
Group {
|
||||
if allowMenu.wrappedValue {
|
||||
if hasImageOrVideo {
|
||||
InteractionView(content:
|
||||
self.environmentObject(ChatModel.shared)
|
||||
.overlay(DetermineWidthImageVideoItem())
|
||||
.onPreferenceChange(DetermineWidthImageVideoItem.Key.self) { itemWidth.wrappedValue = $0 == 0 ? maxWidth : $0 }
|
||||
, maxWidth: maxWidth, itemWidth: itemWidth, menu: menu, backgroundColor: backgroundColor)
|
||||
.frame(maxWidth: itemWidth.wrappedValue)
|
||||
} else {
|
||||
InteractionView(content: self.environmentObject(ChatModel.shared), maxWidth: maxWidth, itemWidth: itemWidth, menu: menu, backgroundColor: backgroundColor)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class HostingViewHolder: UIView {
|
||||
var contentSize: CGSize = CGSizeMake(0, 0)
|
||||
override var intrinsicContentSize: CGSize { get { contentSize } }
|
||||
}
|
||||
|
||||
struct InteractionView<Content: View>: UIViewRepresentable {
|
||||
let content: Content
|
||||
var maxWidth: CGFloat
|
||||
var itemWidth: Binding<CGFloat>
|
||||
@Binding var menu: UIMenu
|
||||
var backgroundColor: Color
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
let view = HostingViewHolder()
|
||||
view.backgroundColor = .clear
|
||||
let hostView = UIHostingController(rootView: content)
|
||||
// hostView.view.backgroundColor = UIColor(backgroundColor)
|
||||
hostView.view.backgroundColor = .clear
|
||||
view.contentSize = hostView.view.intrinsicContentSize
|
||||
hostView.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
let constraints = [
|
||||
hostView.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
hostView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
hostView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
hostView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
hostView.view.widthAnchor.constraint(equalTo: view.widthAnchor),
|
||||
hostView.view.heightAnchor.constraint(equalTo: view.heightAnchor)
|
||||
]
|
||||
view.addSubview(hostView.view)
|
||||
view.addConstraints(constraints)
|
||||
view.layer.cornerRadius = 18
|
||||
hostView.view.layer.cornerRadius = 18
|
||||
let menuInteraction = UIContextMenuInteraction(delegate: context.coordinator)
|
||||
view.addInteraction(menuInteraction)
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIView, context: Context) {
|
||||
let was = (uiView as! HostingViewHolder).contentSize
|
||||
(uiView as! HostingViewHolder).contentSize = uiView.subviews[0].sizeThatFits(CGSizeMake(itemWidth.wrappedValue, .infinity))
|
||||
if was != (uiView as! HostingViewHolder).contentSize {
|
||||
uiView.invalidateIntrinsicContentSize()
|
||||
}
|
||||
//uiView.backgroundColor = UIColor(backgroundColor)
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, UIContextMenuInteractionDelegate {
|
||||
let parent: InteractionView<Content>
|
||||
|
||||
init(_ parent: InteractionView<Content>) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func contextMenuInteraction(
|
||||
_ interaction: UIContextMenuInteraction,
|
||||
configurationForMenuAtLocation location: CGPoint
|
||||
) -> UIContextMenuConfiguration? {
|
||||
UIContextMenuConfiguration(
|
||||
identifier: nil,
|
||||
previewProvider: nil,
|
||||
actionProvider: { [weak self] _ in
|
||||
guard let self = self else { return nil }
|
||||
return self.parent.menu
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// func contextMenuInteraction(
|
||||
// _ interaction: UIContextMenuInteraction,
|
||||
// willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration,
|
||||
// animator: UIContextMenuInteractionCommitAnimating
|
||||
// ) {
|
||||
// animator.addCompletion {
|
||||
// print("user tapped")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -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,7 +12,6 @@ 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
|
||||
@@ -26,12 +25,11 @@ 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))
|
||||
.background(backgroundColor != nil ? backgroundColor! : .clear)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,29 +53,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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -145,7 +144,7 @@ struct PasscodeEntry: View {
|
||||
label()
|
||||
}
|
||||
}
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: size, height: h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,6 @@ struct MigrateFromDevice: View {
|
||||
List {
|
||||
Section {} header: {
|
||||
Text("Stopping chat")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
progressView()
|
||||
@@ -190,16 +189,14 @@ struct MigrateFromDevice: View {
|
||||
Section {
|
||||
Text(reason)
|
||||
Button(action: stopChat) {
|
||||
settingsRow("stop.fill", color: theme.colors.secondary) {
|
||||
settingsRow("stop.fill") {
|
||||
Text("Stop chat").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Error stopping chat")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text("In order to continue, chat should be stopped.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
@@ -218,16 +215,14 @@ struct MigrateFromDevice: View {
|
||||
List {
|
||||
Section {
|
||||
Button(action: { migrationState = .archiving }) {
|
||||
settingsRow("tray.and.arrow.up", color: theme.colors.secondary) {
|
||||
settingsRow("tray.and.arrow.up") {
|
||||
Text("Archive and upload").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Confirm upload")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text("All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
@@ -238,7 +233,6 @@ struct MigrateFromDevice: View {
|
||||
List {
|
||||
Section {} header: {
|
||||
Text("Archiving database")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
progressView()
|
||||
@@ -253,7 +247,6 @@ struct MigrateFromDevice: View {
|
||||
List {
|
||||
Section {} header: {
|
||||
Text("Uploading archive")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
let ratio = Float(uploadedBytes) / Float(totalBytes)
|
||||
@@ -270,16 +263,14 @@ struct MigrateFromDevice: View {
|
||||
Button(action: {
|
||||
migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil)
|
||||
}) {
|
||||
settingsRow("tray.and.arrow.up", color: theme.colors.secondary) {
|
||||
settingsRow("tray.and.arrow.up") {
|
||||
Text("Repeat upload").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Upload failed")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text("You can give another try.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
@@ -293,7 +284,6 @@ struct MigrateFromDevice: View {
|
||||
List {
|
||||
Section {} header: {
|
||||
Text("Creating archive link")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
progressView()
|
||||
@@ -304,12 +294,12 @@ struct MigrateFromDevice: View {
|
||||
List {
|
||||
Section {
|
||||
Button(action: { cancelMigration(fileId, ctrl) }) {
|
||||
settingsRow("multiply", color: theme.colors.secondary) {
|
||||
settingsRow("multiply") {
|
||||
Text("Cancel migration").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
Button(action: { finishMigration(fileId, ctrl) }) {
|
||||
settingsRow("checkmark", color: theme.colors.secondary) {
|
||||
settingsRow("checkmark") {
|
||||
Text("Finalize migration").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
@@ -318,10 +308,9 @@ struct MigrateFromDevice: View {
|
||||
Text("**Warning**: the archive will be removed.")
|
||||
Text("Choose _Migrate from another device_ on the new device and scan QR code.")
|
||||
}
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.font(.callout)
|
||||
}
|
||||
Section(header: Text("Show QR code").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Show QR code") {
|
||||
SimpleXLinkQRCode(uri: link)
|
||||
.padding()
|
||||
.background(
|
||||
@@ -334,7 +323,7 @@ struct MigrateFromDevice: View {
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
}
|
||||
|
||||
Section(header: Text("Or securely share this file link").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Or securely share this file link") {
|
||||
shareLinkView(link)
|
||||
}
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 10))
|
||||
@@ -346,24 +335,22 @@ struct MigrateFromDevice: View {
|
||||
List {
|
||||
Section {
|
||||
Button(action: { alert = .startChat() }) {
|
||||
settingsRow("play.fill", color: theme.colors.secondary) {
|
||||
settingsRow("play.fill") {
|
||||
Text("Start chat").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
Button(action: { alert = .deleteChat() }) {
|
||||
settingsRow("trash.fill", color: theme.colors.secondary) {
|
||||
settingsRow("trash.fill") {
|
||||
Text("Delete database from this device").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Migration complete")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("You **must not** use the same database on two devices.")
|
||||
Text("**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.")
|
||||
}
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
@@ -604,7 +591,6 @@ struct MigrateFromDevice: View {
|
||||
}
|
||||
|
||||
private struct PassphraseConfirmationView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var migrationState: MigrationFromState
|
||||
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
|
||||
@State private var currentKey: String = ""
|
||||
@@ -627,17 +613,15 @@ private struct PassphraseConfirmationView: View {
|
||||
verifyingPassphrase = false
|
||||
}
|
||||
}) {
|
||||
settingsRow(useKeychain ? "key" : "lock", color: theme.colors.secondary) {
|
||||
settingsRow(useKeychain ? "key" : "lock", color: .secondary) {
|
||||
Text("Verify passphrase")
|
||||
}
|
||||
}
|
||||
.disabled(verifyingPassphrase || currentKey.isEmpty)
|
||||
} header: {
|
||||
Text("Verify database passphrase")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text("Confirm that you remember database passphrase to migrate it.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.font(.callout)
|
||||
}
|
||||
.onAppear {
|
||||
|
||||
@@ -181,7 +181,7 @@ struct MigrateToDevice: View {
|
||||
private func pasteOrScanLinkView() -> some View {
|
||||
ZStack {
|
||||
List {
|
||||
Section(header: Text("Scan QR code").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Scan QR code") {
|
||||
ScannerInView(showQRCodeScanner: $showQRCodeScanner) { resp in
|
||||
switch resp {
|
||||
case let .success(r):
|
||||
@@ -198,7 +198,7 @@ struct MigrateToDevice: View {
|
||||
}
|
||||
}
|
||||
if developerTools {
|
||||
Section(header: Text("Or paste archive link").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Or paste archive link") {
|
||||
pasteLinkView()
|
||||
}
|
||||
}
|
||||
@@ -227,7 +227,6 @@ struct MigrateToDevice: View {
|
||||
List {
|
||||
Section {} header: {
|
||||
Text("Downloading link details")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
progressView()
|
||||
@@ -242,7 +241,6 @@ struct MigrateToDevice: View {
|
||||
List {
|
||||
Section {} header: {
|
||||
Text("Downloading archive")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
let ratio = Float(downloadedBytes) / Float(max(totalBytes, 1))
|
||||
@@ -257,16 +255,14 @@ struct MigrateToDevice: View {
|
||||
try? FileManager.default.removeItem(atPath: archivePath)
|
||||
migrationState = .linkDownloading(link: link)
|
||||
}) {
|
||||
settingsRow("tray.and.arrow.down", color: theme.colors.secondary) {
|
||||
settingsRow("tray.and.arrow.down") {
|
||||
Text("Repeat download").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Download failed")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text("You can give another try.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
@@ -282,7 +278,6 @@ struct MigrateToDevice: View {
|
||||
List {
|
||||
Section {} header: {
|
||||
Text("Importing archive")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
progressView()
|
||||
@@ -298,16 +293,14 @@ struct MigrateToDevice: View {
|
||||
Button(action: {
|
||||
migrationState = .archiveImport(archivePath: archivePath)
|
||||
}) {
|
||||
settingsRow("square.and.arrow.down", color: theme.colors.secondary) {
|
||||
settingsRow("square.and.arrow.down") {
|
||||
Text("Repeat import").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Import failed")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text("You can give another try.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
@@ -341,7 +334,7 @@ struct MigrateToDevice: View {
|
||||
Button(action: {
|
||||
migrationState = .migration(passphrase: passphrase, confirmation: confirmation, useKeychain: useKeychain)
|
||||
}) {
|
||||
settingsRow("square.and.arrow.down", color: theme.colors.secondary) {
|
||||
settingsRow("square.and.arrow.down") {
|
||||
Text(button).foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
@@ -350,10 +343,8 @@ struct MigrateToDevice: View {
|
||||
}
|
||||
} header: {
|
||||
Text(header)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text(footer)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
@@ -364,7 +355,6 @@ struct MigrateToDevice: View {
|
||||
List {
|
||||
Section {} header: {
|
||||
Text("Migrating")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
progressView()
|
||||
@@ -392,20 +382,18 @@ struct MigrateToDevice: View {
|
||||
appSettings.networkConfig = updated
|
||||
finishMigration(appSettings)
|
||||
}) {
|
||||
settingsRow("checkmark", color: theme.colors.secondary) {
|
||||
settingsRow("checkmark") {
|
||||
Text("Apply").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Confirm network settings")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text("Please confirm that network settings are correct for this device.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.font(.callout)
|
||||
}
|
||||
|
||||
Section(header: Text("Network settings").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Network settings") {
|
||||
Picker("Use .onion hosts", selection: $onionHosts) {
|
||||
ForEach(OnionHosts.values, id: \.self) { Text($0.text) }
|
||||
}
|
||||
@@ -581,7 +569,6 @@ struct MigrateToDevice: View {
|
||||
}
|
||||
|
||||
private struct PassphraseEnteringView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var migrationState: MigrationToState?
|
||||
@State private var useKeychain = true
|
||||
@State var currentKey: String
|
||||
@@ -593,7 +580,7 @@ private struct PassphraseEnteringView: View {
|
||||
ZStack {
|
||||
List {
|
||||
Section {
|
||||
settingsRow("key", color: theme.colors.secondary) {
|
||||
settingsRow("key", color: .secondary) {
|
||||
Toggle("Save passphrase in Keychain", isOn: $useKeychain)
|
||||
}
|
||||
|
||||
@@ -622,14 +609,13 @@ private struct PassphraseEnteringView: View {
|
||||
verifyingPassphrase = false
|
||||
}
|
||||
}) {
|
||||
settingsRow("key", color: theme.colors.secondary) {
|
||||
settingsRow("key", color: .secondary) {
|
||||
Text("Open chat")
|
||||
}
|
||||
}
|
||||
.disabled(verifyingPassphrase || currentKey.isEmpty)
|
||||
} header: {
|
||||
Text("Enter passphrase")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
if useKeychain {
|
||||
@@ -640,7 +626,6 @@ private struct PassphraseEnteringView: View {
|
||||
Text("**Warning**: Instant push notifications require passphrase saved in Keychain.")
|
||||
}
|
||||
}
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.font(.callout)
|
||||
.padding(.top, 1)
|
||||
.onTapGesture { keyboardVisible = false }
|
||||
|
||||
@@ -71,7 +71,7 @@ struct AddGroupView: View {
|
||||
|
||||
ZStack(alignment: .center) {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
ProfileImage(imageStr: profile.image, size: 128)
|
||||
ProfileImage(imageStr: profile.image, size: 128, color: Color(uiColor: .secondarySystemGroupedBackground))
|
||||
if profile.image != nil {
|
||||
Button {
|
||||
profile.image = nil
|
||||
@@ -105,7 +105,6 @@ struct AddGroupView: View {
|
||||
sharedGroupProfileInfo(incognitoDefault)
|
||||
Text("Fully decentralized – visible only to members.")
|
||||
}
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.onTapGesture(perform: hideKeyboard)
|
||||
}
|
||||
@@ -146,7 +145,7 @@ struct AddGroupView: View {
|
||||
profile.image = nil
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
func groupNameTextField() -> some View {
|
||||
@@ -159,7 +158,7 @@ struct AddGroupView: View {
|
||||
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "pencil").foregroundColor(theme.colors.secondary)
|
||||
Image(systemName: "pencil").foregroundColor(.secondary)
|
||||
}
|
||||
textField("Enter group name…", text: $profile.displayName)
|
||||
.focused($focusDisplayName)
|
||||
@@ -194,7 +193,6 @@ struct AddGroupView: View {
|
||||
let groupMembers = await apiListMembers(gInfo.groupId)
|
||||
await MainActor.run {
|
||||
m.groupMembers = groupMembers.map { GMember.init($0) }
|
||||
m.populateGroupMembersIndexes()
|
||||
}
|
||||
}
|
||||
let c = Chat(chatInfo: .group(groupInfo: gInfo), chatItems: [])
|
||||
|
||||
@@ -10,7 +10,6 @@ import SwiftUI
|
||||
|
||||
enum NewChatMenuOption: Identifiable {
|
||||
case newContact
|
||||
case scanPaste
|
||||
case newGroup
|
||||
|
||||
var id: Self { self }
|
||||
@@ -26,11 +25,6 @@ struct NewChatMenuButton: View {
|
||||
} label: {
|
||||
Text("Add contact")
|
||||
}
|
||||
Button {
|
||||
newChatMenuOption = .scanPaste
|
||||
} label: {
|
||||
Text("Scan / Paste link")
|
||||
}
|
||||
Button {
|
||||
newChatMenuOption = .newGroup
|
||||
} label: {
|
||||
@@ -45,7 +39,6 @@ struct NewChatMenuButton: View {
|
||||
.sheet(item: $newChatMenuOption) { opt in
|
||||
switch opt {
|
||||
case .newContact: NewChatView(selection: .invite)
|
||||
case .scanPaste: NewChatView(selection: .connect, showQRCodeScanner: true)
|
||||
case .newGroup: AddGroupView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,15 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
import CodeScanner
|
||||
import AVFoundation
|
||||
import SimpleXChat
|
||||
|
||||
struct SomeAlert: Identifiable {
|
||||
var alert: Alert
|
||||
var id: String
|
||||
enum SomeAlert: Identifiable {
|
||||
case someAlert(alert: Alert, id: String)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case let .someAlert(_, id): return id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum NewChatViewAlert: Identifiable {
|
||||
@@ -38,7 +42,6 @@ enum NewChatOption: Identifiable {
|
||||
|
||||
struct NewChatView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@State var selection: NewChatOption
|
||||
@State var showQRCodeScanner = false
|
||||
@State private var invitationUsed: Bool = false
|
||||
@@ -91,7 +94,7 @@ struct NewChatView: View {
|
||||
.background(
|
||||
// Rectangle is needed for swipe gesture to work on mostly empty views (creatingLinkProgressView and retryButton)
|
||||
Rectangle()
|
||||
.fill(theme.base == DefaultTheme.LIGHT ? theme.colors.background.asGroupedBackground(theme.base.mode) : theme.colors.background)
|
||||
.fill(AppTheme.shared.colors.background)
|
||||
)
|
||||
.animation(.easeInOut(duration: 0.3333), value: selection)
|
||||
.gesture(DragGesture(minimumDistance: 20.0, coordinateSpace: .local)
|
||||
@@ -110,7 +113,7 @@ struct NewChatView: View {
|
||||
}
|
||||
)
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
.onChange(of: invitationUsed) { used in
|
||||
if used && !(m.showingInvitation?.connChatUsed ?? true) {
|
||||
m.markShowingInvitationUsed()
|
||||
@@ -139,8 +142,8 @@ struct NewChatView: View {
|
||||
switch(a) {
|
||||
case let .planAndConnectAlert(alert):
|
||||
return planAndConnectAlert(alert, dismiss: true, cleanup: { pastedLink = "" })
|
||||
case let .newChatSomeAlert(a):
|
||||
return a.alert
|
||||
case let .newChatSomeAlert(.someAlert(alert, _)):
|
||||
return alert
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,7 +181,7 @@ struct NewChatView: View {
|
||||
await MainActor.run {
|
||||
creatingConnReq = false
|
||||
if let apiAlert = apiAlert {
|
||||
alert = .newChatSomeAlert(alert: SomeAlert(alert: apiAlert, id: "createInvitation error"))
|
||||
alert = .newChatSomeAlert(alert: .someAlert(alert: apiAlert, id: "createInvitation error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,7 +207,6 @@ struct NewChatView: View {
|
||||
|
||||
private struct InviteView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var invitationUsed: Bool
|
||||
@Binding var contactConnection: PendingContactConnection?
|
||||
var connReqInvitation: String
|
||||
@@ -212,7 +214,7 @@ private struct InviteView: View {
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section(header: Text("Share this 1-time invite link").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Share this 1-time invite link") {
|
||||
shareLinkView()
|
||||
}
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 10))
|
||||
@@ -223,7 +225,6 @@ private struct InviteView: View {
|
||||
IncognitoToggle(incognitoEnabled: $incognitoDefault)
|
||||
} footer: {
|
||||
sharedProfileInfo(incognitoDefault)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
.onChange(of: incognitoDefault) { incognito in
|
||||
@@ -260,7 +261,7 @@ private struct InviteView: View {
|
||||
}
|
||||
|
||||
private func qrCodeView() -> some View {
|
||||
Section(header: Text("Or show this code").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Or show this code") {
|
||||
SimpleXLinkQRCode(uri: connReqInvitation, onShare: setInvitationUsed)
|
||||
.padding()
|
||||
.background(
|
||||
@@ -283,7 +284,6 @@ private struct InviteView: View {
|
||||
|
||||
private struct ConnectView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var showQRCodeScanner: Bool
|
||||
@Binding var pastedLink: String
|
||||
@Binding var alert: NewChatViewAlert?
|
||||
@@ -291,10 +291,10 @@ private struct ConnectView: View {
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section(header: Text("Paste the link you received").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Paste the link you received") {
|
||||
pasteLinkView()
|
||||
}
|
||||
Section(header: Text("Or scan QR code").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Or scan QR code") {
|
||||
ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processQRCode)
|
||||
}
|
||||
}
|
||||
@@ -315,7 +315,7 @@ private struct ConnectView: View {
|
||||
// showQRCodeScanner = false
|
||||
connect(pastedLink)
|
||||
} else {
|
||||
alert = .newChatSomeAlert(alert: SomeAlert(
|
||||
alert = .newChatSomeAlert(alert: .someAlert(
|
||||
alert: mkAlert(title: "Invalid link", message: "The text you pasted is not a SimpleX link."),
|
||||
id: "pasteLinkView: code is not a SimpleX link"
|
||||
))
|
||||
@@ -338,14 +338,14 @@ private struct ConnectView: View {
|
||||
if strIsSimplexLink(r.string) {
|
||||
connect(link)
|
||||
} else {
|
||||
alert = .newChatSomeAlert(alert: SomeAlert(
|
||||
alert = .newChatSomeAlert(alert: .someAlert(
|
||||
alert: mkAlert(title: "Invalid QR code", message: "The code you scanned is not a SimpleX link QR code."),
|
||||
id: "processQRCode: code is not a SimpleX link"
|
||||
))
|
||||
}
|
||||
case let .failure(e):
|
||||
logger.error("processQRCode QR code error: \(e.localizedDescription)")
|
||||
alert = .newChatSomeAlert(alert: SomeAlert(
|
||||
alert = .newChatSomeAlert(alert: .someAlert(
|
||||
alert: mkAlert(title: "Invalid QR code", message: "Error scanning code: \(e.localizedDescription)"),
|
||||
id: "processQRCode: failure"
|
||||
))
|
||||
@@ -496,7 +496,7 @@ struct IncognitoToggle: View {
|
||||
ZStack(alignment: .leading) {
|
||||
Image(systemName: incognitoEnabled ? "theatermasks.fill" : "theatermasks")
|
||||
.frame(maxWidth: 24, maxHeight: 24, alignment: .center)
|
||||
.foregroundColor(incognitoEnabled ? Color.indigo : theme.colors.secondary)
|
||||
.foregroundColor(incognitoEnabled ? Color.indigo : .secondary)
|
||||
.font(.system(size: 14))
|
||||
Toggle(isOn: $incognitoEnabled) {
|
||||
HStack(spacing: 6) {
|
||||
|
||||
@@ -27,7 +27,6 @@ enum UserProfileAlert: Identifiable {
|
||||
|
||||
struct CreateProfile: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@State private var displayName: String = ""
|
||||
@FocusState private var focusDisplayName
|
||||
@State private var alert: UserProfileAlert?
|
||||
@@ -46,8 +45,6 @@ struct CreateProfile: View {
|
||||
} header: {
|
||||
HStack {
|
||||
Text("Your profile")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
|
||||
let name = displayName.trimmingCharacters(in: .whitespaces)
|
||||
let validName = mkValidName(name)
|
||||
if name != validName {
|
||||
@@ -65,12 +62,11 @@ struct CreateProfile: View {
|
||||
Text("Your profile, contacts and delivered messages are stored on your device.")
|
||||
Text("The profile is only shared with your contacts.")
|
||||
}
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Create your profile")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
.alert(item: $alert) { a in userProfileAlert(a, $displayName) }
|
||||
.onAppear() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
|
||||
@@ -79,7 +79,7 @@ struct SimpleXInfo: View {
|
||||
MigrateToDevice(migrationState: $m.migrationState)
|
||||
}
|
||||
.navigationTitle("Migrate here")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showHowItWorks) {
|
||||
|
||||
@@ -444,7 +444,6 @@ func shouldShowWhatsNew() -> Bool {
|
||||
|
||||
struct WhatsNewView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@State var currentVersion = versionDescriptions.count - 1
|
||||
@State var currentVersionNav = versionDescriptions.count - 1
|
||||
var viaSettings = false
|
||||
@@ -456,7 +455,7 @@ struct WhatsNewView: View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("New in \(v.version)")
|
||||
.font(.title)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical)
|
||||
ForEach(v.features, id: \.icon) { f in
|
||||
@@ -500,7 +499,7 @@ struct WhatsNewView: View {
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
Image(systemName: icon)
|
||||
.symbolRenderingMode(.monochrome)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(minWidth: 30, alignment: .center)
|
||||
Text(title).font(.title3).bold()
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import CodeScanner
|
||||
|
||||
struct ConnectDesktopView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
var viaSettings = false
|
||||
@AppStorage(DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS) private var deviceName = UIDevice.current.name
|
||||
@@ -168,7 +167,7 @@ struct ConnectDesktopView: View {
|
||||
|
||||
private func connectDesktopView(showScanner: Bool = true) -> some View {
|
||||
List {
|
||||
Section(header: Text("This device name").foregroundColor(theme.colors.secondary)) {
|
||||
Section("This device name") {
|
||||
devicesView()
|
||||
}
|
||||
if showScanner {
|
||||
@@ -179,19 +178,19 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Connect to desktop")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func connectingDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> some View {
|
||||
ZStack {
|
||||
List {
|
||||
Section(header: Text("Connecting to desktop").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Connecting to desktop") {
|
||||
ctrlDeviceNameText(session, rc)
|
||||
ctrlDeviceVersionText(session)
|
||||
}
|
||||
|
||||
if let sessCode = session.sessionCode {
|
||||
Section(header: Text("Session code").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Session code") {
|
||||
sessionCodeText(sessCode)
|
||||
}
|
||||
}
|
||||
@@ -204,15 +203,15 @@ struct ConnectDesktopView: View {
|
||||
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func searchingDesktopView() -> some View {
|
||||
List {
|
||||
Section(header: Text("This device name").foregroundColor(theme.colors.secondary)) {
|
||||
Section("This device name") {
|
||||
devicesView()
|
||||
}
|
||||
Section(header: Text("Found desktop").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Found desktop") {
|
||||
Text("Waiting for desktop...").italic()
|
||||
Button {
|
||||
disconnectDesktop()
|
||||
@@ -222,15 +221,15 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Connecting to desktop")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
@ViewBuilder private func foundDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo, _ compatible: Bool) -> some View {
|
||||
let v = List {
|
||||
Section(header: Text("This device name").foregroundColor(theme.colors.secondary)) {
|
||||
Section("This device name") {
|
||||
devicesView()
|
||||
}
|
||||
Section(header: Text("Found desktop").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Found desktop") {
|
||||
ctrlDeviceNameText(session, rc)
|
||||
ctrlDeviceVersionText(session)
|
||||
if !compatible {
|
||||
@@ -250,7 +249,7 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Found desktop")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
|
||||
if compatible && connectRemoteViaMulticastAuto {
|
||||
v.onAppear { confirmKnownDesktop(rc) }
|
||||
@@ -261,12 +260,12 @@ struct ConnectDesktopView: View {
|
||||
|
||||
private func verifySessionView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?, _ sessCode: String) -> some View {
|
||||
List {
|
||||
Section(header: Text("Connected to desktop").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Connected to desktop") {
|
||||
ctrlDeviceNameText(session, rc)
|
||||
ctrlDeviceVersionText(session)
|
||||
}
|
||||
|
||||
Section(header: Text("Verify code with desktop").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Verify code with desktop") {
|
||||
sessionCodeText(sessCode)
|
||||
Button {
|
||||
verifyDesktopSessionCode(sessCode)
|
||||
@@ -280,7 +279,7 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Verify connection")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text {
|
||||
@@ -302,13 +301,13 @@ struct ConnectDesktopView: View {
|
||||
|
||||
private func activeSessionView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo) -> some View {
|
||||
List {
|
||||
Section(header: Text("Connected desktop").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Connected desktop") {
|
||||
Text(rc.deviceViewName)
|
||||
ctrlDeviceVersionText(session)
|
||||
}
|
||||
|
||||
if let sessCode = session.sessionCode {
|
||||
Section(header: Text("Session code").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Session code") {
|
||||
sessionCodeText(sessCode)
|
||||
}
|
||||
}
|
||||
@@ -318,11 +317,10 @@ struct ConnectDesktopView: View {
|
||||
} footer: {
|
||||
// This is specific to iOS
|
||||
Text("Keep the app open to use it from desktop")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Connected to desktop")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func sessionCodeText(_ code: String) -> some View {
|
||||
@@ -343,13 +341,13 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
|
||||
private func scanDesctopAddressView() -> some View {
|
||||
Section(header: Text("Scan QR code from desktop").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Scan QR code from desktop") {
|
||||
ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processDesktopQRCode, scanMode: .oncePerCode)
|
||||
}
|
||||
}
|
||||
|
||||
private func desktopAddressView() -> some View {
|
||||
Section(header: Text("Desktop address").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Desktop address") {
|
||||
if sessionAddress.isEmpty {
|
||||
Button {
|
||||
sessionAddress = UIPasteboard.general.string ?? ""
|
||||
@@ -362,7 +360,7 @@ struct ConnectDesktopView: View {
|
||||
Text(sessionAddress).lineLimit(1)
|
||||
Spacer()
|
||||
Image(systemName: "multiply.circle.fill")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
.onTapGesture { sessionAddress = "" }
|
||||
}
|
||||
}
|
||||
@@ -377,7 +375,7 @@ struct ConnectDesktopView: View {
|
||||
|
||||
private func linkedDesktopsView() -> some View {
|
||||
List {
|
||||
Section(header: Text("Desktop devices").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Desktop devices") {
|
||||
ForEach(remoteCtrls, id: \.remoteCtrlId) { rc in
|
||||
remoteCtrlView(rc)
|
||||
}
|
||||
@@ -388,7 +386,7 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Linked desktop options").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Linked desktop options") {
|
||||
Toggle("Verify connections", isOn: $confirmRemoteSessions)
|
||||
Toggle("Discover via local network", isOn: $connectRemoteViaMulticast)
|
||||
if connectRemoteViaMulticast {
|
||||
@@ -397,7 +395,7 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Linked desktops")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func remoteCtrlView(_ rc: RemoteCtrlInfo) -> some View {
|
||||
|
||||
@@ -122,7 +122,6 @@ struct TerminalView: View {
|
||||
return ScrollView {
|
||||
Text(s.prefix(maxItemSize))
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
|
||||
@@ -24,7 +24,6 @@ enum NetworkSettingsAlert: Identifiable {
|
||||
}
|
||||
|
||||
struct AdvancedNetworkSettings: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@State private var netCfg = NetCfg.defaults
|
||||
@State private var currentNetCfg = NetCfg.defaults
|
||||
@State private var cfgLoaded = false
|
||||
@@ -70,11 +69,10 @@ struct AdvancedNetworkSettings: View {
|
||||
Text("TCP_KEEPINTVL")
|
||||
Text("TCP_KEEPCNT")
|
||||
}
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} header: {
|
||||
Text("")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
HStack {
|
||||
Button {
|
||||
|
||||
@@ -10,7 +10,6 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct CallSettings: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@AppStorage(DEFAULT_WEBRTC_POLICY_RELAY) private var webrtcPolicyRelay = true
|
||||
@AppStorage(GROUP_DEFAULT_CALL_KIT_ENABLED, store: groupDefaults) private var callKitEnabled = true
|
||||
@AppStorage(DEFAULT_CALL_KIT_CALLS_IN_RECENTS) private var callKitCallsInRecents = false
|
||||
@@ -23,21 +22,18 @@ struct CallSettings: View {
|
||||
NavigationLink {
|
||||
RTCServers()
|
||||
.navigationTitle("Your ICE servers")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text("WebRTC ICE servers")
|
||||
}
|
||||
Toggle("Always use relay", isOn: $webrtcPolicyRelay)
|
||||
} header: {
|
||||
Text("Settings")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
if webrtcPolicyRelay {
|
||||
Text("Relay server protects your IP address, but it can observe the duration of the call.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} else {
|
||||
Text("Relay server is only used if necessary. Another party can observe your IP address.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +47,6 @@ struct CallSettings: View {
|
||||
}
|
||||
} header: {
|
||||
Text("Interface")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
if callKitEnabled {
|
||||
Text("You can accept calls from lock screen, without device and app authentication.")
|
||||
@@ -61,7 +56,7 @@ struct CallSettings: View {
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Limitations").foregroundColor(theme.colors.secondary)) {
|
||||
Section("Limitations") {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
textListItem("1.", "Do NOT use SimpleX for emergency calls.")
|
||||
textListItem("2.", "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.")
|
||||
|
||||
@@ -10,7 +10,6 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct DeveloperView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@@ -24,26 +23,24 @@ struct DeveloperView: View {
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
.opacity(0.5)
|
||||
.colorMultiply(theme.colors.secondary)
|
||||
Text("Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)")
|
||||
.padding(.leading, 36)
|
||||
}
|
||||
NavigationLink {
|
||||
TerminalView()
|
||||
} label: {
|
||||
settingsRow("terminal", color: theme.colors.secondary) { Text("Chat console") }
|
||||
settingsRow("terminal") { Text("Chat console") }
|
||||
}
|
||||
settingsRow("internaldrive", color: theme.colors.secondary) {
|
||||
settingsRow("internaldrive") {
|
||||
Toggle("Confirm database upgrades", isOn: $confirmDatabaseUpgrades)
|
||||
}
|
||||
settingsRow("chevron.left.forwardslash.chevron.right", color: theme.colors.secondary) {
|
||||
settingsRow("chevron.left.forwardslash.chevron.right") {
|
||||
Toggle("Show developer options", isOn: $developerTools)
|
||||
}
|
||||
} header: {
|
||||
Text("")
|
||||
} footer: {
|
||||
((developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option."))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
(developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ struct HiddenProfileView: View {
|
||||
@State var user: User
|
||||
@Binding var profileHidden: Bool
|
||||
@EnvironmentObject private var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@State private var hidePassword = ""
|
||||
@State private var confirmHidePassword = ""
|
||||
@@ -37,7 +36,7 @@ struct HiddenProfileView: View {
|
||||
PassphraseField(key: $hidePassword, placeholder: "Password to show", valid: passwordValid, showStrength: true)
|
||||
PassphraseField(key: $confirmHidePassword, placeholder: "Confirm password", valid: confirmValid)
|
||||
|
||||
settingsRow("lock", color: theme.colors.secondary) {
|
||||
settingsRow("lock") {
|
||||
Button("Save profile password") {
|
||||
Task {
|
||||
do {
|
||||
@@ -59,10 +58,8 @@ struct HiddenProfileView: View {
|
||||
.disabled(saveDisabled)
|
||||
} header: {
|
||||
Text("Hidden profile password")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text("To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.font(.body)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
@@ -73,7 +70,7 @@ struct HiddenProfileView: View {
|
||||
message: Text(savePasswordError ?? "")
|
||||
)
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
var passwordValid: Bool { hidePassword == hidePassword.trimmingCharacters(in: .whitespaces) }
|
||||
|
||||