Compare commits

..

5 Commits

Author SHA1 Message Date
Avently ed16f71ee4 creating wallpaper dir at launch 2024-06-20 09:55:41 +01:00
Avently 5bc70bdfed creating directory for wallpapers 2024-06-20 09:55:41 +01:00
Avently fb4927ca87 divided types per target 2024-06-20 09:55:41 +01:00
Avently 2023464a13 types and api 2024-06-20 09:55:41 +01:00
Stanislav Dmitrenko b8bf5871fb ios: wallpapers (#4304)
* ios: wallpapers

* theme selection

* applied theme colors and preset wallpaper

* more places with background

* one more

* accent color

* defaults

* rename

* background

* no change to cell color

* unneeded

* changes

* no global tint

* defaults

* removed unneeded class

* for merging
2024-06-20 09:51:29 +01:00
278 changed files with 2779 additions and 18735 deletions
+9 -8
View File
@@ -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,
-65
View File
@@ -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"
},
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

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"
},
Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

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"
},
Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

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"
},
Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

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"
},
Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

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"
},
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

+5 -9
View File
@@ -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)
}
}
}
+14 -34
View File
@@ -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
}
+9 -32
View File
@@ -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
+22 -101
View File
@@ -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
)
}
}
+19 -59
View File
@@ -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())
}
}
+14 -163
View File
@@ -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)
}
}
+23 -52
View File
@@ -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 {
File diff suppressed because it is too large Load Diff
@@ -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()
}
}
+21 -21
View File
@@ -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 {
-1
View File
@@ -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 {
File diff suppressed because it is too large Load Diff
@@ -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) }

Some files were not shown because too many files have changed in this diff Show More