Compare commits

..

42 Commits

Author SHA1 Message Date
Evgeny Poberezkin 9bc4a9a19b icon 2024-07-03 14:31:40 +01:00
Evgeny Poberezkin e34faf56c7 Merge branch 'master' into lp/ios-translate-sheet 2024-07-03 14:13:56 +01:00
spaced4ndy 1180edf80e ios: servers summary (#4321)
* types

* chat list buttons

* update

* wip

* xftp summary

* sub status view

* reconnect server button

* reset stats button

* refactor

* subscription icon in server view

* getAgentSubsSummary api

* view

* subs indicator with timer

* rename

* variableValueAsPercentage

* encodePrettyPrinted

* totals, timer

* reduce interval

* show more/less stats

* refactor

* sort

* rework indicator wip

* change offline icon

* rework indicator

* show percent

* file progress

* fix

* onion

* user servers, icons

* dashes

* Revert "user servers, icons"

This reverts commit 805e7e9bd6.

* remove icon, remove total, remove session %

* exclude sessions

* move starting from

* file stats

* fix

* open server settings

* file in progress

* update

* wpi

* wip

* rework stats

* arrow for sessions

* texts

* reconnect all

* single user

* rework file stats

* update

* dont log terminal items

* center

* update

* _connSubIgnored

* rename

* update

* large titles

* refactor

* update

* text

* upd

* single dash
2024-07-03 13:59:46 +01:00
Levitating Pineapple 5dad4e22f5 Minor 2024-07-03 14:03:49 +02:00
spaced4ndy 4bd6517d19 core: servers stats improvements, fixes (#4358) 2024-07-03 15:58:37 +04:00
Levitating Pineapple f1290d7e38 Feat: Add system translate sheet to chat item menu 2024-07-03 13:47:46 +02:00
Evgeny Poberezkin 2585f4ecfd ios: ChatView performance improvements (#4353)
* feat: Add synthesized hashable conformance to chat and API types (#4348)

* UIKit ReverseList

* ReverseList - manual layout updates for external state

* Propagate ScrollModel; Disable async media width

* Filter chat items

* Remove UIKit menu wrapper

* Make chat item width calculation synchronous (#4371)

* Fix floating button regression

* Improve filter performance

* Fix page load the merged items exceed full page

* Resolve iOS15 compabibility

* Restore build config

* Add page-up scroll; Fix same item decrementing unread counter multiple times

* Fix: Chat not loading additional pages, if newest items are all merged and exceed page size

* Minor

* Fix item loading regression

* Fix item loading regression 2

* Fix unread regression

* refactor

---------

Co-authored-by: Arturs Krumins <auth@levitatingpineapple.com>
Co-authored-by: Levitating Pineapple <noreply@levitatingpineapple.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-07-03 10:24:26 +01:00
Evgeny Poberezkin e22e01acd2 5.8.2: ios 226, android 223, desktop 55 2024-07-02 21:28:34 +01:00
Stanislav Dmitrenko 8e4299afb6 android, desktop: close modals only for desktop (#4390) 2024-07-02 18:26:23 +01:00
Stanislav Dmitrenko ecff3c6ee5 android, desktop: notifications improvement (missed call, hidden user) (#4389)
* android, desktop: notifications improvement (missed call, hidden user)

* change

* change
2024-07-02 18:19:43 +01:00
Evgeny Poberezkin 85af368371 core: 5.8.2.0, simplexmq: 5.8.2.0 2024-07-02 15:07:46 +01:00
Stanislav Dmitrenko 0d3928bd51 android, desktop: handle situation when not all databases created before shut down (#4294)
* android, desktop: handle situation when not all databases created before shut down

* more logic of choosing whether to delete databases or not

* comment

* rename

* refactoring
2024-07-02 15:02:36 +01:00
Evgeny Poberezkin ddeaa1c7c3 core: servers 2024-07-02 14:50:25 +01:00
Evgeny Poberezkin 00ba468898 core: update simplexmq (disable fast handshake) (#4388)
* core: update simplexmq (disable fast handshake)

* fix encoding tests

* fix

* update simplexmq
2024-07-02 14:35:47 +01:00
spaced4ndy 593c7d247c ui: add ServerEnabled type (#4381) 2024-07-01 17:10:22 +04:00
Evgeny Poberezkin 5907d8bd0c core: remove legacy agent stats (#4375) 2024-07-01 11:25:27 +01:00
sh 231082860e flatpak: verify app (#4379) 2024-07-01 09:34:38 +01:00
Evgeny Poberezkin c6366cb269 Merge branch 'stable' 2024-06-29 16:53:30 +01:00
sergeyshadok d066c66282 Update XFTP-SERVER.md (#4184)
Systemd itself uses %<something> tokens so %% needs to be used for specifiers

(https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html)
2024-06-29 16:50:28 +01:00
Evgeny Poberezkin fab3827697 core: fix markdown for trailing / and ) characters in the links (#4357) 2024-06-29 08:41:21 +01:00
Evgeny Poberezkin d8126e0cd0 website: update join team 2024-06-28 19:09:01 +01:00
sh 5baf8789d6 flatpak: add desktop and metainfo (#4366)
* flatpak: add desktop and metainfo

* metainfo: update

* corrections

* ampersand

* shorter

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-06-28 10:09:34 +01:00
Evgeny Poberezkin dd5454f390 Merge branch 'stable' 2024-06-28 00:12:10 +01:00
Evgeny Poberezkin fca098f535 docs: update donation addresses 2024-06-28 00:10:51 +01:00
spaced4ndy fba0478e50 docs: more additions to chat protocol (#4355)
* docs: more additions to chat protocol

* schema wip

* schema

* update diagram

* remove comments

* punctuation

* diagram text

* update threat model

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-06-25 08:37:54 +01:00
spaced4ndy d951003191 core: servers summary api (#4319)
* core: servers summary api

* rework

* server known types

* set stats file path

* rename

* local simplexmq

* update

* rfc

* update

* update

* get servers

* compile summary

* remove sort

* rename

* rename, refactor

* refactor attempt

* refactor attempt 2

* refactor

* fix

* fix2

* remove space

* refactor xftp

* update

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-06-25 09:51:55 +04:00
spaced4ndy 1af513c548 docs: update chat protocol (#4344) 2024-06-23 20:36:25 +01:00
Evgeny Poberezkin 85dc467f74 5.8.1: ios 225, android 221, desktop 54 2024-06-19 23:41:23 +01:00
spaced4ndy 89e65c2fc1 multiplatform: check preferences before share/forward (#4338) 2024-06-19 18:53:45 +04:00
Evgeny Poberezkin 0fa8d77214 ios: fix search disappearing in forward view (#4337)
* ios: fix search disappearing in forward view

* update

* diff

* refactor

* use List
2024-06-19 12:51:56 +01:00
spaced4ndy 6ba82c8c39 ios: check preferences before forward (#4336)
* ios: check preferences before forward

* rework

* clean up

* shorter message

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-06-19 13:49:44 +04:00
Evgeny Poberezkin e19c14b249 ios: update core library to 5.8.1 2024-06-19 10:12:22 +01:00
Evgeny Poberezkin d993381d31 Revert "web: glossary fix (#4298)"
This reverts commit d3274b28ba.
2024-06-18 23:44:58 +01:00
Evgeny Poberezkin 0198c1f110 core: 5.8.1.0 2024-06-18 23:12:27 +01:00
spaced4ndy 6c4c9b172c core: fix markdown check (#4334)
* core: fix markdown

* more tests

* enable tests

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-06-18 12:44:33 -07:00
spaced4ndy d9a61af401 core: fix preference application (#4330) 2024-06-18 10:37:45 +04:00
spaced4ndy a58522ef15 core: api to reconnect single server (#4317) 2024-06-17 11:52:55 +04:00
Evgeny Poberezkin bcd510c913 cli: option to confirm up migrations (#4150)
* cli: option to confirm up migrations

* fix test
2024-06-16 20:24:37 -07:00
M. Sarmad Qadeer c4083e00c5 web: update feed (#4255)
* web: update feed

* change date filter in feed.rss
2024-06-16 14:31:36 -07:00
M. Sarmad Qadeer 7cd35b9068 web: design fixes eg: blog titles & link wrap (#4305)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-06-16 14:22:53 -07:00
M. Sarmad Qadeer d3274b28ba web: glossary fix (#4298) 2024-06-16 08:23:40 -07:00
Narasimha-sc 40bd5dc2da web: update copyright year 2023->2024 (#4297)
* Update copyright year

* Update ar.json

* Update bg.json

* Update cs.json

* Update de.json

* Update es.json

* Update fr.json

* Update hu.json

* Update it.json

* Update ja.json

* Update nl.json

* Update pl.json

* Update pt_BR.json

* Update ru.json
2024-06-16 08:19:43 -07:00
185 changed files with 3800 additions and 3564 deletions
+8 -9
View File
@@ -158,20 +158,19 @@ 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) - it is commission-free for us.
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies.
- [GitHub](https://github.com/sponsors/simplex-chat) (commission-free) or [OpenCollective](https://opencollective.com/simplex-chat) (~10% commission).
- Bitcoin: bc1qd74rc032ek2knhhr3yjq2ajzc5enz3h4qwnxad
- Monero: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Bitcoin: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- BCH: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- BCH: bitcoincash:qq6c8vfvxqrk6rhdysgvkhqc24sggkfsx5nqvdlqcg
- Ethereum: 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692
- USDT:
- BNB Smart Chain: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
- Tron: TNnTrKLBmdy2Wn3cAQR98dAVvWhLskQGfW
- Ethereum: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
- Solana: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L
- Ethereum: 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692
- Solana: 7JCf5m3TiHmYKZVr6jCu1KeZVtb9Y1jRMQDU69p5ARnu
- please ask if you want to donate any other coins.
Thank you,
+2 -9
View File
@@ -17,7 +17,6 @@ class AppDelegate: NSObject, UIApplicationDelegate {
if #available(iOS 17.0, *) { trackKeyboard() }
NotificationCenter.default.addObserver(self, selector: #selector(pasteboardChanged), name: UIPasteboard.changedNotification, object: nil)
removePasscodesIfReinstalled()
prepareForLaunch()
return true
}
@@ -142,10 +141,6 @@ class AppDelegate: NSObject, UIApplicationDelegate {
}
}
private func prepareForLaunch() {
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
}
static func keepScreenOn(_ on: Bool) {
UIApplication.shared.isIdleTimerDisabled = on
}
@@ -153,15 +148,13 @@ class AppDelegate: NSObject, UIApplicationDelegate {
class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
var window: UIWindow?
static var windowStatic: UIWindow?
var windowScene: UIWindowScene?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
UITableView.appearance().backgroundColor = .clear
guard let windowScene = scene as? UIWindowScene else { return }
self.windowScene = windowScene
window = windowScene.keyWindow
SceneDelegate.windowStatic = windowScene.keyWindow
ThemeManager.applyTheme(currentThemeDefault.get())
window?.tintColor = UIColor(cgColor: getUIAccentColorDefault())
window?.overrideUserInterfaceStyle = getUserInterfaceStyleDefault()
}
}
@@ -1,21 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "wallpaper_cats@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

@@ -1,21 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "wallpaper_flowers@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

@@ -1,21 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "wallpaper_hearts@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

@@ -1,21 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "wallpaper_kids@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

@@ -1,21 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "wallpaper_school@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

@@ -1,21 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "wallpaper_travel@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

+2 -21
View File
@@ -14,8 +14,6 @@ struct ContentView: View {
@ObservedObject var alertManager = AlertManager.shared
@ObservedObject var callController = CallController.shared
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
@EnvironmentObject var sceneDelegate: SceneDelegate
var contentAccessAuthenticationExtended: Bool
@@ -53,16 +51,6 @@ struct ContentView: View {
}
var body: some View {
if #available(iOS 16.0, *) {
allViews()
.scrollContentBackground(.hidden)
} else {
// on iOS 15 scroll view background disabled in SceneDelegate
allViews()
}
}
@ViewBuilder func allViews() -> some View {
ZStack {
let showCallArea = chatModel.activeCall != nil && chatModel.activeCall?.callState != .waitCapabilities && chatModel.activeCall?.callState != .invitationAccepted
// contentView() has to be in a single branch, so that enabling authentication doesn't trigger re-rendering and close settings.
@@ -108,8 +96,6 @@ struct ContentView: View {
initializationView()
}
}
//.tint(theme.colors.primary)
.background(theme.colors.background)
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
.sheet(isPresented: $showSettings) {
SettingsView(showSettings: $showSettings)
@@ -152,11 +138,6 @@ struct ContentView: View {
break
}
}
.onChange(of: colorScheme) { scheme in
if sceneDelegate.window?.overrideUserInterfaceStyle == .unspecified {
reactOnDarkThemeChanges(scheme == .dark)
}
}
}
@ViewBuilder private func contentView() -> some View {
@@ -243,8 +224,8 @@ struct ContentView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity )
.background(
Rectangle()
.fill(theme.colors.background)
)
.fill(.background)
)
}
private func mainView() -> some View {
+10 -18
View File
@@ -332,12 +332,12 @@ final class ChatModel: ObservableObject {
private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
if let i = getChatItemIndex(cItem) {
withAnimation {
withConditionalAnimation {
_updateChatItem(at: i, with: cItem)
}
return false
} else {
withAnimation(itemAnimation()) {
withConditionalAnimation(itemAnimation()) {
var ci = cItem
if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
ci.meta.itemStatus = status
@@ -357,7 +357,7 @@ final class ChatModel: ObservableObject {
func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem, status: CIStatus? = nil) {
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
withAnimation {
withConditionalAnimation {
_updateChatItem(at: i, with: cItem)
}
} else if let status = status {
@@ -422,16 +422,6 @@ final class ChatModel: ObservableObject {
}
}
func updateCurrentUserUiThemes(uiThemes: ThemeModeOverrides?) {
guard var current = currentUser else { return }
current.uiThemes = uiThemes
let i = users.firstIndex(where: { $0.user.userId == current.userId })
if let i {
users[i].user = current
}
currentUser = current
}
func addLiveDummy(_ chatInfo: ChatInfo) -> ChatItem {
let cItem = ChatItem.liveDummy(chatInfo.chatType)
withAnimation {
@@ -522,11 +512,13 @@ 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) {
markChatItemRead_(i)
if reversedChatItems[i].isRcvNew {
// update current chat
markChatItemRead_(i)
// update preview
decreaseUnreadCounter(cInfo)
}
}
}
@@ -733,7 +725,7 @@ struct NTFContactRequest {
var chatId: String
}
struct UnreadChatItemCounts {
struct UnreadChatItemCounts: Equatable {
var totalBelow: Int
var unreadBelow: Int
}
-37
View File
@@ -205,43 +205,6 @@ func moveTempFileFromURL(_ url: URL) -> CryptoFile? {
}
}
func saveWallpaperFile(url: URL) -> String? {
let destFile = URL(fileURLWithPath: generateNewFileName(getWallpaperDirectory().path + "/" + "wallpaper", "jpg", fullPath: true))
do {
try FileManager.default.copyItem(atPath: url.path, toPath: destFile.path)
return destFile.lastPathComponent
} catch {
logger.error("FileUtils.saveWallpaperFile error: \(error.localizedDescription)")
return nil
}
}
func saveWallpaperFile(image: UIImage) -> String? {
let hasAlpha = imageHasAlpha(image)
let destFile = URL(fileURLWithPath: generateNewFileName(getWallpaperDirectory().path + "/" + "wallpaper", hasAlpha ? "png" : "jpg", fullPath: true))
let dataResized = resizeImageToDataSize(image, maxDataSize: 5_000_000, hasAlpha: hasAlpha)
do {
try dataResized!.write(to: destFile)
return destFile.lastPathComponent
} catch {
logger.error("FileUtils.saveWallpaperFile error: \(error.localizedDescription)")
return nil
}
}
func removeWallpaperFile(fileName: String? = nil) {
do {
try FileManager.default.contentsOfDirectory(atPath: getWallpaperDirectory().path).forEach {
if URL(fileURLWithPath: $0).lastPathComponent == fileName { try FileManager.default.removeItem(atPath: $0) }
}
} catch {
logger.error("FileUtils.removeWallpaperFile error: \(error.localizedDescription)")
}
if let fileName {
WallpaperType.cachedImages.removeValue(forKey: fileName)
}
}
func generateNewFileName(_ prefix: String, _ ext: String, fullPath: Bool = false) -> String {
uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)", fullPath: fullPath)
}
+44 -28
View File
@@ -92,18 +92,22 @@ 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) -> ChatResponse {
logger.debug("chatSendCmd \(cmd.cmdType)")
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)")
}
let start = Date.now
let resp = bgTask
? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd, ctrl) }
: sendSimpleXCmd(cmd, ctrl)
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)
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)
}
}
return resp
}
@@ -242,8 +246,14 @@ func apiSuspendChat(timeoutMicroseconds: Int) {
logger.error("apiSuspendChat error: \(String(describing: r))")
}
func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String, ctrl: chat_ctrl? = nil) throws {
let r = chatSendCmdSync(.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder), ctrl)
func apiSetTempFolder(tempFolder: String, ctrl: chat_ctrl? = nil) throws {
let r = chatSendCmdSync(.setTempFolder(tempFolder: tempFolder), ctrl)
if case .cmdOk = r { return }
throw r
}
func apiSetFilesFolder(filesFolder: String, ctrl: chat_ctrl? = nil) throws {
let r = chatSendCmdSync(.setFilesFolder(filesFolder: filesFolder), ctrl)
if case .cmdOk = r { return }
throw r
}
@@ -303,8 +313,10 @@ 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: 50), search: search))
let r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: .last(count: loadItemsPerPage), search: search))
if case let .apiChat(_, chat) = r { return Chat.init(chat) }
throw r
}
@@ -535,6 +547,11 @@ 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))
}
@@ -825,21 +842,6 @@ func apiSetConnectionAlias(connId: Int64, localAlias: String) async throws -> Pe
throw r
}
func apiSetUserUIThemes(userId: Int64, themes: ThemeModeOverrides?) async -> Bool {
let r = await chatSendCmd(.apiSetUserUIThemes(userId: userId, themes: themes))
if case .cmdOk = r { return true }
logger.error("apiSetUserUIThemes bad response: \(String(describing: r))")
return false
}
func apiSetChatUIThemes(chatId: ChatId, themes: ThemeModeOverrides?) async -> Bool {
let r = await chatSendCmd(.apiSetChatUIThemes(chatId: chatId, themes: themes))
if case .cmdOk = r { return true }
logger.error("apiSetChatUIThemes bad response: \(String(describing: r))")
return false
}
func apiCreateUserAddress() async throws -> String {
let userId = try currentUserId("apiCreateUserAddress")
let r = await chatSendCmd(.apiCreateMyAddress(userId: userId))
@@ -1343,6 +1345,18 @@ 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
@@ -1362,7 +1376,8 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni
if encryptionStartedDefault.get() {
encryptionStartedDefault.set(false)
}
try apiSetAppFilePaths(filesFolder: getAppFilesDirectory().path, tempFolder: getTempFilesDirectory().path, assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path)
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
m.chatInitialized = true
m.currentUser = try apiGetActiveUser()
@@ -1447,7 +1462,8 @@ func startChatWithTemporaryDatabase(ctrl: chat_ctrl) throws -> User? {
logger.debug("startChatWithTemporaryDatabase")
let migrationActiveUser = try? apiGetActiveUser(ctrl: ctrl) ?? apiCreateActiveUser(Profile(displayName: "Temp", fullName: ""), ctrl: ctrl)
try setNetworkConfig(getNetCfg(), ctrl: ctrl)
try apiSetAppFilePaths(filesFolder: getMigrationTempFilesDirectory().path, tempFolder: getMigrationTempFilesDirectory().path, assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path, ctrl: ctrl)
try apiSetTempFolder(tempFolder: getMigrationTempFilesDirectory().path, ctrl: ctrl)
try apiSetFilesFolder(filesFolder: getMigrationTempFilesDirectory().path, ctrl: ctrl)
_ = try apiStartChat(ctrl: ctrl)
return migrationActiveUser
}
-1
View File
@@ -39,7 +39,6 @@ struct SimpleXApp: App {
// so that it's computed by the time view renders, and not on event after rendering
ContentView(contentAccessAuthenticationExtended: !authenticationExpired())
.environmentObject(chatModel)
.environmentObject(AppTheme.shared)
.onOpenURL { url in
logger.debug("ContentView.onOpenURL: \(url)")
chatModel.appOpenUrl = url
-121
View File
@@ -1,121 +0,0 @@
//
// ThemeShared.swift
// SimpleX (iOS)
//
// Created by Avently on 14.06.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import Foundation
import SwiftUI
import SimpleXChat
var CurrentColors: ThemeManager.ActiveTheme = ThemeManager.currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) {
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 {
static let shared = AppTheme(name: CurrentColors.name, base: CurrentColors.base, colors: CurrentColors.colors, appColors: CurrentColors.appColors, wallpaper: CurrentColors.wallpaper)
var name: String
var base: DefaultTheme
@ObservedObject var colors: Colors
@ObservedObject var appColors: AppColors
@ObservedObject var wallpaper: AppWallpaper
init(name: String, base: DefaultTheme, colors: Colors, appColors: AppColors, wallpaper: AppWallpaper) {
self.name = name
self.base = base
self.colors = colors
self.appColors = appColors
self.wallpaper = wallpaper
}
}
struct ThemedBackground: ViewModifier {
@EnvironmentObject var theme: AppTheme
func body(content: Content) -> some View {
content
.background(
theme.base == DefaultTheme.SIMPLEX
? LinearGradient(
colors: [
theme.colors.background.lighter(0.4),
theme.colors.background.darker(0.4)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
: LinearGradient(
colors: [],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.background(
theme.base == DefaultTheme.SIMPLEX
? Color.clear
: theme.colors.background
)
}
}
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)
}
}
extension ThemeWallpaper {
public func importFromString() -> ThemeWallpaper {
if preset == nil, let image {
// Need to save image from string and to save its path
if let data = Data(base64Encoded: dropImagePrefix(image)),
let parsed = UIImage(data: data),
let filename = saveWallpaperFile(image: parsed) {
var copy = self
copy.image = nil
copy.imageFile = filename
return copy
} else {
return ThemeWallpaper()
}
} else {
return self
}
}
func withFilledWallpaperBase64() -> ThemeWallpaper {
let aw = toAppWallpaper()
let type = aw.type
let preset: String? = if case let WallpaperType.Preset(filename, _) = type { filename } else { nil }
let scale: Float? = if case let WallpaperType.Preset(_, scale) = type { scale } else { if case let WallpaperType.Image(_, scale, _) = type { scale } else { 1.0 } }
let scaleType: WallpaperScaleType? = if case let WallpaperType.Image(_, _, scaleType) = type { scaleType } else { nil }
let image: String? = if case WallpaperType.Image = type, let image = type.uiImage { resizeImageToStrSize(image, maxDataSize: 5_000_000) } else { nil }
return ThemeWallpaper (
preset: preset,
scale: scale,
scaleType: scaleType,
background: aw.background?.toReadableHex(),
tint: aw.tint?.toReadableHex(),
image: image,
imageFile: nil
)
}
}
-263
View File
@@ -1,263 +0,0 @@
//
// ThemeManager.swift
// SimpleX (iOS)
//
// Created by Avently on 03.06.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import Foundation
import SwiftUI
import SimpleXChat
class ThemeManager {
struct ActiveTheme {
let name: String
let base: DefaultTheme
let colors: Colors
let appColors: AppColors
var wallpaper: AppWallpaper = AppWallpaper(background: nil, tint: nil, type: .Empty)
}
private static func systemDarkThemeColors() -> (Colors, DefaultTheme) {
switch systemDarkThemeDefault.get() {
case DefaultTheme.DARK.themeName: (DarkColorPalette, DefaultTheme.DARK)
case DefaultTheme.SIMPLEX.themeName: (SimplexColorPalette, DefaultTheme.SIMPLEX)
case DefaultTheme.BLACK.themeName: (BlackColorPalette, DefaultTheme.BLACK)
default: (SimplexColorPalette, DefaultTheme.SIMPLEX)
}
}
private static func nonSystemThemeName() -> String {
let themeName = currentThemeDefault.get()
return if themeName != DefaultTheme.SYSTEM_THEME_NAME {
themeName
} else {
systemInDarkThemeCurrently ? systemDarkThemeDefault.get() : DefaultTheme.LIGHT.themeName
}
}
static func defaultActiveTheme(_ appSettingsTheme: [ThemeOverrides]) -> ThemeOverrides? {
let nonSystemThemeName = nonSystemThemeName()
let defaultThemeId = currentThemeIdsDefault.get()[nonSystemThemeName]
return appSettingsTheme.getTheme(defaultThemeId)
}
static func defaultActiveTheme(_ perUserTheme: ThemeModeOverrides?, _ appSettingsTheme: [ThemeOverrides]) -> ThemeModeOverride {
let perUserTheme = !CurrentColors.colors.isLight ? perUserTheme?.dark : perUserTheme?.light
if let perUserTheme {
return perUserTheme
}
let defaultTheme = defaultActiveTheme(appSettingsTheme)
return ThemeModeOverride(mode: CurrentColors.base.mode, colors: defaultTheme?.colors ?? ThemeColors(), wallpaper: defaultTheme?.wallpaper)
}
static func currentColors(_ themeOverridesForType: WallpaperType?, _ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverrides?, _ appSettingsTheme: [ThemeOverrides]) -> ActiveTheme {
let themeName = currentThemeDefault.get()
let nonSystemThemeName = nonSystemThemeName()
let defaultTheme = defaultActiveTheme(appSettingsTheme)
let baseTheme = switch nonSystemThemeName {
case DefaultTheme.LIGHT.themeName: ActiveTheme(name: DefaultTheme.LIGHT.themeName, base: DefaultTheme.LIGHT, colors: LightColorPalette.clone(), appColors: LightColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.LIGHT)))
case DefaultTheme.DARK.themeName: ActiveTheme(name: DefaultTheme.DARK.themeName, base: DefaultTheme.DARK, colors: DarkColorPalette.clone(), appColors: DarkColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.DARK)))
case DefaultTheme.SIMPLEX.themeName: ActiveTheme(name: DefaultTheme.SIMPLEX.themeName, base: DefaultTheme.SIMPLEX, colors: SimplexColorPalette.clone(), appColors: SimplexColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.SIMPLEX)))
case DefaultTheme.BLACK.themeName: ActiveTheme(name: DefaultTheme.BLACK.themeName, base: DefaultTheme.BLACK, colors: BlackColorPalette.clone(), appColors: BlackColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.BLACK)))
default: ActiveTheme(name: DefaultTheme.LIGHT.themeName, base: DefaultTheme.LIGHT, colors: LightColorPalette.clone(), appColors: LightColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.LIGHT)))
}
let perUserTheme = baseTheme.colors.isLight ? perUserTheme?.light : perUserTheme?.dark
let theme = appSettingsTheme.sameTheme(themeOverridesForType ?? perChatTheme?.type ?? perUserTheme?.type ?? defaultTheme?.wallpaper?.toAppWallpaper().type, nonSystemThemeName) ?? defaultTheme
if theme == nil && perUserTheme == nil && perChatTheme == nil && themeOverridesForType == nil {
return ActiveTheme(name: themeName, base: baseTheme.base, colors: baseTheme.colors, appColors: baseTheme.appColors, wallpaper: baseTheme.wallpaper)
}
let presetWallpaperTheme: ThemeColors? = if let wallpaper = perChatTheme?.wallpaper {
if let preset = wallpaper.preset { PresetWallpaper.from(preset)?.colors[baseTheme.base] } else { nil }
} else if let wallpaper = perUserTheme?.wallpaper {
if let preset = wallpaper.preset { PresetWallpaper.from(preset)?.colors[baseTheme.base] } else { nil }
} else {
if let preset = theme?.wallpaper?.preset { PresetWallpaper.from(preset)?.colors[baseTheme.base] } else { nil }
}
let themeOrEmpty = theme ?? ThemeOverrides(base: baseTheme.base)
let colors = themeOrEmpty.toColors(themeOrEmpty.base, perChatTheme?.colors, perUserTheme?.colors, presetWallpaperTheme)
return ActiveTheme(
name: themeName,
base: baseTheme.base,
colors: colors,
appColors: themeOrEmpty.toAppColors(themeOrEmpty.base, perChatTheme?.colors, perChatTheme?.type, perUserTheme?.colors, perUserTheme?.type, presetWallpaperTheme),
wallpaper: themeOrEmpty.toAppWallpaper(themeOverridesForType, perChatTheme, perUserTheme, colors.background)
)
}
static func currentThemeOverridesForExport(_ 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 {
nil
} else {
ThemeWallpaper.from(wType, wBackground?.toReadableHex(), wTint?.toReadableHex()).withFilledWallpaperBase64()
}
return ThemeOverrides(
themeId: "",
base: current.base,
colors: ThemeColors.from(current.colors, current.appColors),
wallpaper: w
)
}
static func applyTheme(_ theme: String) {
currentThemeDefault.set(theme)
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
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 changeDarkTheme(_ theme: String) {
systemDarkThemeDefault.set(theme)
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
}
static func saveAndApplyThemeColor(_ baseTheme: DefaultTheme, _ name: ThemeColor, _ color: Color? = nil, _ pref: CodableDefault<[ThemeOverrides]>? = nil) {
let nonSystemThemeName = baseTheme.themeName
let pref = pref ?? themeOverridesDefault
let overrides = pref.get()
let themeId = currentThemeIdsDefault.get()[nonSystemThemeName]
let prevValue = overrides.getTheme(themeId) ?? ThemeOverrides(base: baseTheme)
pref.set(overrides.replace(prevValue.withUpdatedColor(name, color?.toReadableHex())))
var themeIds = currentThemeIdsDefault.get()
themeIds[nonSystemThemeName] = prevValue.themeId
currentThemeIdsDefault.set(themeIds)
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
}
static func applyThemeColor(name: ThemeColor, color: Color? = nil, pref: Binding<ThemeModeOverride>) {
pref.wrappedValue = pref.wrappedValue.withUpdatedColor(name, color?.toReadableHex())
}
static func saveAndApplyWallpaper(_ baseTheme: DefaultTheme, _ type: WallpaperType?, _ pref: CodableDefault<[ThemeOverrides]>?) {
let nonSystemThemeName = baseTheme.themeName
let pref = pref ?? themeOverridesDefault
let overrides = pref.get()
let theme = overrides.sameTheme(type, baseTheme.themeName)
var prevValue = theme ?? ThemeOverrides(base: baseTheme)
prevValue.wallpaper = if let type {
if case WallpaperType.Empty = type {
nil as ThemeWallpaper?
} else {
ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint)
}
} else {
nil
}
pref.set(overrides.replace(prevValue))
var themeIds = currentThemeIdsDefault.get()
themeIds[nonSystemThemeName] = prevValue.themeId
currentThemeIdsDefault.set(themeIds)
CurrentColors = currentColors( nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
}
static func copyFromSameThemeOverrides(_ type: WallpaperType?, _ lowerLevelOverride: ThemeModeOverride?, _ pref: Binding<ThemeModeOverride>) -> Bool {
let overrides = themeOverridesDefault.get()
let sameWallpaper: ThemeWallpaper? = if let wallpaper = lowerLevelOverride?.wallpaper, lowerLevelOverride?.type?.sameType(type) == true {
wallpaper
} else {
overrides.sameTheme(type, CurrentColors.base.themeName)?.wallpaper
}
guard let sameWallpaper else {
if let type {
var w: ThemeWallpaper = ThemeWallpaper.from(type, nil, nil)
w.scale = nil
w.scaleType = nil
pref.wrappedValue = ThemeModeOverride(mode: CurrentColors.base.mode, wallpaper: w)
} else {
// Make an empty wallpaper to override any top level ones
pref.wrappedValue = ThemeModeOverride(mode: CurrentColors.base.mode, wallpaper: ThemeWallpaper())
}
return true
}
var type = sameWallpaper.toAppWallpaper().type
if case let WallpaperType.Image(filename, scale, scaleType) = type, sameWallpaper.imageFile == filename {
// same image file. Needs to be copied first in order to be able to remove the file once it's not needed anymore without affecting main theme override
if let filename = saveWallpaperFile(url: getWallpaperFilePath(filename)) {
type = WallpaperType.Image(filename, scale, scaleType)
} else {
logger.error("Error while copying wallpaper from global overrides to chat overrides")
return false
}
}
var prevValue = pref.wrappedValue
var w = ThemeWallpaper.from(type, nil, nil)
w.scale = nil
w.scaleType = nil
prevValue.colors = ThemeColors()
prevValue.wallpaper = w
pref.wrappedValue = prevValue
return true
}
static func applyWallpaper(_ type: WallpaperType?, _ pref: Binding<ThemeModeOverride>) {
var prevValue = pref.wrappedValue
prevValue.wallpaper = if let type {
ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint)
} else {
nil
}
pref.wrappedValue = prevValue
}
static func saveAndApplyThemeOverrides(_ theme: ThemeOverrides, _ pref: CodableDefault<[ThemeOverrides]>? = nil) {
let wallpaper = theme.wallpaper?.importFromString()
let nonSystemThemeName = theme.base.themeName
let pref: CodableDefault<[ThemeOverrides]> = pref ?? themeOverridesDefault
let overrides = pref.get()
var prevValue = overrides.getTheme(nil, wallpaper?.toAppWallpaper().type, theme.base) ?? ThemeOverrides(base: theme.base)
if let imageFile = prevValue.wallpaper?.imageFile {
try? FileManager.default.removeItem(at: getWallpaperFilePath(imageFile))
}
prevValue.base = theme.base
prevValue.colors = theme.colors
prevValue.wallpaper = wallpaper
pref.set(overrides.replace(prevValue))
currentThemeDefault.set(nonSystemThemeName)
var currentThemeIds = currentThemeIdsDefault.get()
currentThemeIds[nonSystemThemeName] = prevValue.themeId
currentThemeIdsDefault.set(currentThemeIds)
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
}
static func resetAllThemeColors(_ pref: CodableDefault<[ThemeOverrides]>? = nil) {
let nonSystemThemeName = nonSystemThemeName()
let pref: CodableDefault<[ThemeOverrides]> = pref ?? themeOverridesDefault
let overrides = pref.get()
guard let themeId = currentThemeIdsDefault.get()[nonSystemThemeName],
var prevValue = overrides.getTheme(themeId)
else { return }
prevValue.colors = ThemeColors()
prevValue.wallpaper?.background = nil
prevValue.wallpaper?.tint = nil
pref.set(overrides.replace(prevValue))
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
}
static func resetAllThemeColors(_ pref: Binding<ThemeModeOverride>) {
var prevValue = pref.wrappedValue
prevValue.colors = ThemeColors()
prevValue.wallpaper?.background = nil
prevValue.wallpaper?.tint = nil
pref.wrappedValue = prevValue
}
static func removeTheme(_ themeId: String?) {
var themes = themeOverridesDefault.get().map { $0 }
themes.removeAll(where: { $0.themeId == themeId })
themeOverridesDefault.set(themes)
}
}
@@ -2,7 +2,7 @@
// MPVolumeView.swift
// SimpleX (iOS)
//
// Created by Avently on 24.04.2024.
// Created by Stanislav on 24.04.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
@@ -11,7 +11,6 @@ import SimpleXChat
struct IncomingCallView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@ObservedObject var cc = CallController.shared
var body: some View {
@@ -44,7 +43,7 @@ struct IncomingCallView: View {
cc.endCall(invitation: invitation)
}
callButton("Ignore", "multiply", theme.colors.primary) {
callButton("Ignore", "multiply", .accentColor) {
cc.activeCallInvitation = nil
}
@@ -64,7 +63,7 @@ struct IncomingCallView: View {
.padding(.horizontal, 16)
.padding(.vertical, 12)
.frame(maxWidth: .infinity)
.modifier(ThemedBackground())
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.onAppear { dismissAllSheets() }
}
@@ -90,7 +90,6 @@ enum SendReceipts: Identifiable, Hashable {
struct ChatInfoView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@ObservedObject var chat: Chat
@State var contact: Contact
@@ -242,7 +241,6 @@ struct ChatInfoView: View {
}
}
}
.modifier(ThemedBackground())
.navigationBarHidden(true)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
@@ -372,7 +370,6 @@ struct ChatInfoView: View {
)
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("Security code")
.modifier(ThemedBackground())
} label: {
Label(
contact.verified ? "View security code" : "Verify security code",
@@ -389,7 +386,6 @@ struct ChatInfoView: View {
currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences)
)
.navigationBarTitle("Contact preferences")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
Label("Contact preferences", systemImage: "switch.2")
@@ -438,7 +434,7 @@ struct ChatInfoView: View {
HStack {
Text("Network status")
Image(systemName: "info.circle")
.foregroundColor(theme.colors.primary)
.foregroundColor(.accentColor)
.font(.system(size: 14))
Spacer()
Text(chatModel.contactNetworkStatus(contact).statusString)
@@ -11,7 +11,6 @@ import SimpleXChat
struct CICallItemView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
var chatItem: ChatItem
var status: CICallStatus
@@ -36,7 +35,7 @@ struct CICallItemView: View {
case .error: missedCallIcon(sent).foregroundColor(.orange)
}
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary, showStatus: false, showEdited: false)
CIMetaView(chat: chat, chatItem: chatItem, showStatus: false, showEdited: false)
.padding(.bottom, 8)
.padding(.horizontal, 12)
}
@@ -11,7 +11,6 @@ import SimpleXChat
struct CIFeaturePreferenceView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
var chatItem: ChatItem
var feature: ChatFeature
var allowed: FeatureAllowed
@@ -46,7 +45,7 @@ struct CIFeaturePreferenceView: View {
r = r
+ Text(acceptText)
.fontWeight(.medium)
.foregroundColor(theme.colors.primary)
.foregroundColor(.accentColor)
+ Text(" ")
}
r = r + chatItem.timestampText
@@ -12,7 +12,6 @@ import SimpleXChat
struct CIFileView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
let file: CIFile?
let edited: Bool
@@ -171,7 +170,7 @@ struct CIFileView: View {
case .sndWarning: fileIcon("doc.fill", innerIcon: "exclamationmark.triangle.fill", innerIconSize: 10)
case .rcvInvitation:
if fileSizeValid(file) {
fileIcon("arrow.down.doc.fill", color: theme.colors.primary)
fileIcon("arrow.down.doc.fill", color: .accentColor)
} else {
fileIcon("doc.fill", color: .orange, innerIcon: "exclamationmark", innerIconSize: 12)
}
@@ -183,7 +182,7 @@ struct CIFileView: View {
progressView()
}
case .rcvAborted:
fileIcon("doc.fill", color: theme.colors.primary, innerIcon: "exclamationmark.arrow.circlepath", innerIconSize: 12)
fileIcon("doc.fill", color: .accentColor, innerIcon: "exclamationmark.arrow.circlepath", innerIconSize: 12)
case .rcvComplete: fileIcon("doc.fill")
case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
case .rcvError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
@@ -11,7 +11,7 @@ import SimpleXChat
struct CIGroupInvitationView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.colorScheme) var colorScheme
@ObservedObject var chat: Chat
var chatItem: ChatItem
var groupInvitation: CIGroupInvitation
@@ -42,7 +42,7 @@ struct CIGroupInvitationView: View {
.overlay(DetermineWidth())
(
Text(chatIncognito ? "Tap to join incognito" : "Tap to join")
.foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : theme.colors.primary)
.foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : .accentColor)
.font(.callout)
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
@@ -65,11 +65,11 @@ struct CIGroupInvitationView: View {
}
}
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary, showStatus: false, showEdited: false)
CIMetaView(chat: chat, chatItem: chatItem, showStatus: false, showEdited: false)
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(chatItemFrameColor(chatItem, theme))
.background(chatItemFrameColor(chatItem, colorScheme))
.cornerRadius(18)
.textSelection(.disabled)
.onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
@@ -99,7 +99,7 @@ struct CIGroupInvitationView: View {
private func groupInfoView(_ action: Bool) -> some View {
var color: Color
if action && !inProgress {
color = chatIncognito ? .indigo : theme.colors.primary
color = chatIncognito ? .indigo : .accentColor
} else {
color = Color(uiColor: .tertiaryLabel)
}
@@ -13,10 +13,9 @@ struct CIImageView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
let chatItem: ChatItem
let image: String
var preview: UIImage?
let maxWidth: CGFloat
@Binding var imgWidth: CGFloat?
@State var scrollProxy: ScrollViewProxy?
var imgWidth: CGFloat?
@State private var showFullScreenImage = false
var body: some View {
@@ -25,15 +24,14 @@ struct CIImageView: View {
if let uiImage = getLoadedImage(file) {
imageView(uiImage)
.fullScreenCover(isPresented: $showFullScreenImage) {
FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage, scrollProxy: scrollProxy)
FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage)
}
.onTapGesture { showFullScreenImage = true }
.onChange(of: m.activeCallViewIsCollapsed) { _ in
showFullScreenImage = false
}
} else if let data = Data(base64Encoded: dropImagePrefix(image)),
let uiImage = UIImage(data: data) {
imageView(uiImage)
} else if let preview {
imageView(preview)
.onTapGesture {
if let file = file {
switch file.fileStatus {
@@ -90,7 +88,6 @@ 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)
@@ -44,7 +44,6 @@ func invalidJSONView(_ json: String) -> some View {
}
.frame(maxHeight: .infinity)
.padding()
.modifier(ThemedBackground())
}
struct CIInvalidJSONView_Previews: PreviewProvider {
@@ -11,7 +11,6 @@ import SimpleXChat
struct CIMemberCreatedContactView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
var chatItem: ChatItem
var body: some View {
@@ -44,7 +43,7 @@ struct CIMemberCreatedContactView: View {
r = r
+ Text(openText)
.fontWeight(.medium)
.foregroundColor(theme.colors.primary)
.foregroundColor(.accentColor)
+ Text(" ")
}
r = r + chatItem.timestampText
@@ -11,9 +11,8 @@ import SimpleXChat
struct CIMetaView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
var chatItem: ChatItem
var metaColor: Color
var metaColor = Color.secondary
var paleMetaColor = Color(UIColor.tertiaryLabel)
var showStatus = true
var showEdited = true
@@ -64,7 +63,6 @@ func ciMetaText(
chatTTL: Int?,
encrypted: Bool?,
color: Color = .clear,
primaryColor: Color = .accentColor,
transparent: Bool = false,
sent: SentCheckmark? = nil,
showStatus: Bool = true,
@@ -87,7 +85,7 @@ func ciMetaText(
r = r + statusIconText("arrow.forward", color.opacity(0.67)).font(.caption2)
}
if showStatus {
if let (icon, statusColor) = meta.statusIcon(color, primaryColor) {
if let (icon, statusColor) = meta.statusIcon(color) {
let t = Text(Image(systemName: icon)).font(.caption2)
let gap = Text(" ").kerning(-1.25)
let t1 = t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67))
@@ -114,16 +112,15 @@ private func statusIconText(_ icon: String, _ color: Color) -> Text {
}
struct CIMetaView_Previews: PreviewProvider {
static let metaColor = Color.secondary
static var previews: some View {
Group {
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete)), metaColor: metaColor)
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial)), metaColor: metaColor)
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete)), metaColor: metaColor)
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial)), metaColor: metaColor)
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete)), metaColor: metaColor)
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true), metaColor: metaColor)
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(), metaColor: metaColor)
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete)))
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial)))
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete)))
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial)))
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete)))
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true))
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample())
}
.previewLayout(.fixed(width: 360, height: 100))
}
@@ -13,7 +13,6 @@ let decryptErrorReason: LocalizedStringKey = "It can happen when you or your con
struct CIRcvDecryptionError: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
var msgDecryptError: MsgDecryptError
var msgCount: UInt32
@@ -115,18 +114,18 @@ struct CIRcvDecryptionError: View {
}
(
Text(Image(systemName: "exclamationmark.arrow.triangle.2.circlepath"))
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
.foregroundColor(syncSupported ? .accentColor : .secondary)
.font(.callout)
+ Text(" ")
+ Text("Fix connection")
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
.foregroundColor(syncSupported ? .accentColor : .secondary)
.font(.callout)
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
)
}
.padding(.horizontal, 12)
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
CIMetaView(chat: chat, chatItem: chatItem)
.padding(.horizontal, 12)
}
.onTapGesture(perform: { onClick() })
@@ -146,7 +145,7 @@ struct CIRcvDecryptionError: View {
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
}
.padding(.horizontal, 12)
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
CIMetaView(chat: chat, chatItem: chatItem)
.padding(.horizontal, 12)
}
.onTapGesture(perform: { onClick() })
@@ -15,14 +15,12 @@ struct CIVideoView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
private let chatItem: ChatItem
private let image: String
private let preview: UIImage?
@State private var duration: Int
@State private var progress: Int = 0
@State private var videoPlaying: Bool = false
private let maxWidth: CGFloat
@Binding private var videoWidth: CGFloat?
@State private var scrollProxy: ScrollViewProxy?
@State private var preview: UIImage? = nil
private var videoWidth: CGFloat?
@State private var player: AVPlayer?
@State private var fullPlayer: AVPlayer?
@State private var url: URL?
@@ -33,13 +31,12 @@ struct CIVideoView: View {
@State private var fullScreenTimeObserver: Any? = nil
@State private var publisher: AnyCancellable? = nil
init(chatItem: ChatItem, image: String, duration: Int, maxWidth: CGFloat, videoWidth: Binding<CGFloat?>, scrollProxy: ScrollViewProxy?) {
init(chatItem: ChatItem, preview: UIImage?, duration: Int, maxWidth: CGFloat, videoWidth: CGFloat?) {
self.chatItem = chatItem
self.image = image
self.preview = preview
self._duration = State(initialValue: duration)
self.maxWidth = maxWidth
self._videoWidth = videoWidth
self.scrollProxy = scrollProxy
self.videoWidth = videoWidth
if let url = getLoadedVideo(chatItem.file) {
let decrypted = chatItem.file?.fileSource?.cryptoArgs == nil ? url : chatItem.file?.fileSource?.decryptedGet()
self._urlDecrypted = State(initialValue: decrypted)
@@ -49,10 +46,6 @@ 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 {
@@ -63,9 +56,8 @@ 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 data = Data(base64Encoded: dropImagePrefix(image)),
let uiImage = UIImage(data: data) {
imageView(uiImage)
} else if let preview {
imageView(preview)
.onTapGesture {
if let file = file {
switch file.fileStatus {
@@ -152,7 +144,6 @@ 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)
@@ -252,7 +243,6 @@ 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()
@@ -11,7 +11,6 @@ import SimpleXChat
struct CIVoiceView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
var chatItem: ChatItem
let recordingFile: CIFile?
let duration: Int
@@ -93,7 +92,7 @@ struct CIVoiceView: View {
}
private func metaView() -> some View {
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
CIMetaView(chat: chat, chatItem: chatItem)
}
}
@@ -119,7 +118,7 @@ struct VoiceMessagePlayerTime: View {
struct VoiceMessagePlayer: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.colorScheme) var colorScheme
var chatItem: ChatItem
var recordingFile: CIFile?
var recordingTime: TimeInterval
@@ -245,7 +244,7 @@ struct VoiceMessagePlayer: View {
.foregroundColor(color)
.padding(.leading, image == "play.fill" ? 4 : 0)
.frame(width: 56, height: 56)
.background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear)
.background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear)
.clipShape(Circle())
if recordingTime > 0 {
ProgressCircle(length: recordingTime, progress: $playbackTime)
@@ -267,7 +266,6 @@ struct VoiceMessagePlayer: View {
}
private struct ProgressCircle: View {
@EnvironmentObject var theme: AppTheme
var length: TimeInterval
@Binding var progress: TimeInterval?
@@ -275,7 +273,7 @@ struct VoiceMessagePlayer: View {
Circle()
.trim(from: 0, to: ((progress ?? TimeInterval(0)) / length))
.stroke(
theme.colors.primary,
Color.accentColor,
style: StrokeStyle(lineWidth: 3)
)
.rotationEffect(.degrees(-90))
@@ -290,7 +288,7 @@ struct VoiceMessagePlayer: View {
.frame(width: size, height: size)
.foregroundColor(Color(uiColor: .tertiaryLabel))
.frame(width: 56, height: 56)
.background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear)
.background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear)
.clipShape(Circle())
}
@@ -298,7 +296,7 @@ struct VoiceMessagePlayer: View {
ProgressView()
.frame(width: 30, height: 30)
.frame(width: 56, height: 56)
.background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear)
.background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear)
.clipShape(Circle())
}
@@ -10,7 +10,7 @@ import SwiftUI
import SimpleXChat
struct DeletedItemView: View {
@EnvironmentObject var theme: AppTheme
@Environment(\.colorScheme) var colorScheme
@ObservedObject var chat: Chat
var chatItem: ChatItem
@@ -19,12 +19,12 @@ struct DeletedItemView: View {
Text(chatItem.content.text)
.foregroundColor(.secondary)
.italic()
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
CIMetaView(chat: chat, chatItem: chatItem)
.padding(.horizontal, 12)
}
.padding(.leading, 12)
.padding(.vertical, 6)
.background(chatItemFrameColor(chatItem, theme))
.background(chatItemFrameColor(chatItem, colorScheme))
.cornerRadius(18)
.textSelection(.disabled)
}
@@ -11,7 +11,6 @@ import SimpleXChat
struct EmojiItemView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
var chatItem: ChatItem
var body: some View {
@@ -19,7 +18,7 @@ struct EmojiItemView: View {
emojiText(chatItem.content.text)
.padding(.top, 8)
.padding(.horizontal, 6)
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
CIMetaView(chat: chat, chatItem: chatItem)
.padding(.bottom, 8)
.padding(.horizontal, 12)
}
@@ -9,17 +9,25 @@
import SwiftUI
import SimpleXChat
let notesChatColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.21)
let notesChatColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.19)
let sentColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12)
let sentColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.17)
private let sentQuoteColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.11)
private let sentQuoteColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.09)
struct FramedItemView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@EnvironmentObject var scrollModel: ReverseListScrollModel<ChatItem>
@Environment(\.colorScheme) var colorScheme
@ObservedObject var chat: Chat
var chatItem: ChatItem
var preview: UIImage?
@Binding var revealed: Bool
var maxWidth: CGFloat = .infinity
@State var scrollProxy: ScrollViewProxy? = nil
@State var msgWidth: CGFloat = 0
@State var imgWidth: CGFloat? = nil
@State var videoWidth: CGFloat? = nil
var imgWidth: CGFloat? = nil
var videoWidth: CGFloat? = nil
@State var metaColor = Color.secondary
@State var showFullScreenImage = false
@Binding var allowMenu: Bool
@@ -51,10 +59,9 @@ struct FramedItemView: View {
if let qi = chatItem.quotedItem {
ciQuoteView(qi)
.onTapGesture {
if let proxy = scrollProxy,
let ci = m.reversedChatItems.first(where: { $0.id == qi.itemId }) {
if let ci = m.reversedChatItems.first(where: { $0.id == qi.itemId }) {
withAnimation {
proxy.scrollTo(ci.viewId, anchor: .bottom)
scrollModel.scrollToItem(id: ci.id)
}
}
}
@@ -76,10 +83,8 @@ struct FramedItemView: View {
.accessibilityLabel("")
}
}
.onAppear {
metaColor = metaColor == .secondary ? theme.colors.secondary : metaColor
}
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, theme))
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, colorScheme))
.background(Color(.systemBackground))
.cornerRadius(18)
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
@@ -110,8 +115,8 @@ struct FramedItemView: View {
.padding(.bottom, 2)
} else {
switch (chatItem.content.msgContent) {
case let .image(text, image):
CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy)
case let .image(text, _):
CIImageView(chatItem: chatItem, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth)
.overlay(DetermineWidth())
if text == "" && !chatItem.meta.isLive {
Color.clear
@@ -123,8 +128,8 @@ struct FramedItemView: View {
} else {
ciMsgContentView(chatItem)
}
case let .video(text, image, duration):
CIVideoView(chatItem: chatItem, image: image, duration: duration, maxWidth: maxWidth, videoWidth: $videoWidth, scrollProxy: scrollProxy)
case let .video(text, _, duration):
CIVideoView(chatItem: chatItem, preview: preview, duration: duration, maxWidth: maxWidth, videoWidth: videoWidth)
.overlay(DetermineWidth())
if text == "" && !chatItem.meta.isLive {
Color.clear
@@ -171,13 +176,12 @@ struct FramedItemView: View {
.font(.caption)
.lineLimit(1)
}
.foregroundColor(theme.colors.secondary)
.foregroundColor(.secondary)
.padding(.horizontal, 12)
.padding(.top, 6)
.padding(.bottom, pad || (chatItem.quotedItem == nil && chatItem.meta.itemForwarded == nil) ? 6 : 0)
.overlay(DetermineWidth())
.frame(minWidth: msgWidth, alignment: .leading)
.background(chatItemFrameContextColor(chatItem, theme))
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
v.frame(maxWidth: mediaWidth, alignment: .leading)
} else {
@@ -229,7 +233,7 @@ struct FramedItemView: View {
// if enable this always, size of the framed voice message item will be incorrect after end of playback
.overlay { if case .voice = chatItem.content.msgContent {} else { DetermineWidth() } }
.frame(minWidth: msgWidth, alignment: .leading)
.background(chatItemFrameContextColor(chatItem, theme))
.background(chatItemFrameContextColor(chatItem, colorScheme))
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
v.frame(maxWidth: mediaWidth, alignment: .leading)
@@ -244,7 +248,7 @@ struct FramedItemView: View {
VStack(alignment: .leading, spacing: 2) {
Text(sender)
.font(.caption)
.foregroundColor(theme.colors.secondary)
.foregroundColor(.secondary)
.lineLimit(1)
ciQuotedMsgTextView(qi, lines: 2)
}
@@ -359,22 +363,22 @@ func onlyImageOrVideo(_ ci: ChatItem) -> Bool {
return false
}
func chatItemFrameColorMaybeImageOrVideo(_ ci: ChatItem, _ theme: AppTheme) -> Color {
func chatItemFrameColorMaybeImageOrVideo(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
onlyImageOrVideo(ci)
? Color.clear
: chatItemFrameColor(ci, theme)
: chatItemFrameColor(ci, colorScheme)
}
func chatItemFrameColor(_ ci: ChatItem, _ theme: AppTheme) -> Color {
func chatItemFrameColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
ci.chatDir.sent
? theme.appColors.sentMessage
: theme.appColors.receivedMessage
? (colorScheme == .light ? sentColorLight : sentColorDark)
: Color(uiColor: .tertiarySystemGroupedBackground)
}
func chatItemFrameContextColor(_ ci: ChatItem, _ theme: AppTheme) -> Color {
func chatItemFrameContextColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
ci.chatDir.sent
? theme.appColors.sentQuote
: theme.appColors.receivedQuote
? (colorScheme == .light ? sentQuoteColorLight : sentQuoteColorDark)
: Color(uiColor: .quaternarySystemFill)
}
struct FramedItemView_Previews: PreviewProvider {
@@ -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,9 +71,7 @@ struct FullScreenMediaView: View {
let w = abs(t.width)
if t.height > 60 && t.height > w * 2 {
showView = false
if let proxy = scrollProxy {
proxy.scrollTo(chatItem.viewId)
}
scrollModel.scrollToItem(id: chatItem.id)
} else if w > 60 && w > abs(t.height) * 2 && !scrolling {
let previous = t.width > 0
scrolling = true
@@ -11,7 +11,6 @@ import SimpleXChat
struct IntegrityErrorItemView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
var msgError: MsgErrorType
var chatItem: ChatItem
@@ -55,7 +54,6 @@ struct IntegrityErrorItemView: View {
struct CIMsgError: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
var chatItem: ChatItem
var onTap: () -> Void
@@ -64,7 +62,7 @@ struct CIMsgError: View {
Text(chatItem.content.text)
.foregroundColor(.red)
.italic()
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
CIMetaView(chat: chat, chatItem: chatItem)
.padding(.horizontal, 12)
}
.padding(.leading, 12)
@@ -11,7 +11,7 @@ import SimpleXChat
struct MarkedDeletedItemView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.colorScheme) var colorScheme
@ObservedObject var chat: Chat
var chatItem: ChatItem
@Binding var revealed: Bool
@@ -22,7 +22,7 @@ struct MarkedDeletedItemView: View {
.foregroundColor(.secondary)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(chatItemFrameColor(chatItem, theme))
.background(chatItemFrameColor(chatItem, colorScheme))
.cornerRadius(18)
.textSelection(.disabled)
}
@@ -19,6 +19,9 @@ 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 {
@@ -35,52 +38,29 @@ 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 {
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))
}
List {
searchFieldView(text: $searchText, focussed: $searchFocused)
.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))
}
.padding(.horizontal)
.padding(.vertical, 8)
.background(Color(uiColor: .systemBackground))
.cornerRadius(12)
.padding(.horizontal)
}
.modifier(ThemedBackground())
} else {
ZStack {
emptyList()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.modifier(ThemedBackground())
emptyList()
}
}
}
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 {
private func foundChat(_ chat: Chat, _ searchStr: String) -> Bool {
let cInfo = chat.chatInfo
return switch cInfo {
case let .direct(contact):
@@ -96,42 +76,70 @@ struct ChatItemForwardingView: View {
}
}
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
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
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(.secondary)
.frame(maxWidth: .infinity)
}
@ViewBuilder private func forwardListNavLinkView(_ chat: Chat) -> some View {
@ViewBuilder private func forwardListChatView(_ chat: Chat) -> some View {
let prohibited = prohibitedByPref(chat)
Button {
dismiss()
if chat.id == fromChatInfo.id {
composeState = ComposeState(
message: composeState.message,
preview: composeState.linkPreview != nil ? composeState.preview : .noPreview,
contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo)
)
if prohibited {
alert = SomeAlert(
alert: mkAlert(
title: "Cannot forward message",
message: "Selected chat preferences prohibit this message."
),
id: "forward prohibited by preferences"
)
} else {
composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo)
chatModel.chatId = chat.id
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
}
}
} label: {
HStack {
ChatInfoImage(chat: chat, size: 30)
.padding(.trailing, 2)
Text(chat.chatInfo.chatViewName)
.foregroundColor(.primary)
.foregroundColor(prohibited ? .secondary : .primary)
.lineLimit(1)
if chat.chatInfo.incognito {
Spacer()
@@ -147,6 +155,27 @@ 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"),
@@ -12,7 +12,7 @@ import SimpleXChat
struct ChatItemInfoView: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.dismiss) var dismiss
@EnvironmentObject var theme: AppTheme
@Environment(\.colorScheme) var colorScheme
var ci: ChatItem
@Binding var chatItemInfo: ChatItemInfo?
@State private var selection: CIInfoTab = .history
@@ -101,14 +101,12 @@ struct ChatItemInfoView: View {
Label("History", systemImage: "clock")
}
.tag(CIInfoTab.history)
.modifier(ThemedBackground())
if let qi = ci.quotedItem {
quoteTab(qi)
.tabItem {
Label("In reply to", systemImage: "arrowshape.turn.up.left")
}
.tag(CIInfoTab.quote)
.modifier(ThemedBackground())
}
if let forwardedFromItem = chatItemInfo?.forwardedFromChatItem {
forwardedFromTab(forwardedFromItem)
@@ -116,7 +114,6 @@ struct ChatItemInfoView: View {
Label(local ? "Saved" : "Forwarded", systemImage: "arrowshape.turn.up.forward")
}
.tag(CIInfoTab.forwarded)
.modifier(ThemedBackground())
}
}
.onAppear {
@@ -126,7 +123,6 @@ struct ChatItemInfoView: View {
}
} else {
historyTab()
.modifier(ThemedBackground())
}
}
@@ -231,7 +227,7 @@ struct ChatItemInfoView: View {
textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(chatItemFrameColor(ci, theme))
.background(chatItemFrameColor(ci, colorScheme))
.cornerRadius(18)
.contextMenu {
if itemVersion.msgContent.text != "" {
@@ -300,7 +296,7 @@ struct ChatItemInfoView: View {
textBubble(qi.text, qi.formattedText, qi.getSender(nil))
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(quotedMsgFrameColor(qi, theme))
.background(quotedMsgFrameColor(qi, colorScheme))
.cornerRadius(18)
.contextMenu {
if qi.text != "" {
@@ -324,10 +320,10 @@ struct ChatItemInfoView: View {
.frame(maxWidth: maxWidth, alignment: .leading)
}
func quotedMsgFrameColor(_ qi: CIQuote, _ theme: AppTheme) -> Color {
func quotedMsgFrameColor(_ qi: CIQuote, _ colorScheme: ColorScheme) -> Color {
(qi.chatDir?.sent ?? false)
? theme.appColors.sentMessage
: theme.appColors.receivedMessage
? (colorScheme == .light ? sentColorLight : sentColorDark)
: Color(uiColor: .tertiarySystemGroupedBackground)
}
@ViewBuilder private func forwardedFromTab(_ forwardedFromItem: AChatItem) -> some View {
@@ -449,7 +445,7 @@ struct ChatItemInfoView: View {
.foregroundColor(.secondary).opacity(0.67)
}
let v = Group {
if let (icon, statusColor) = status.statusIcon(theme.colors.secondary, theme.colors.primary) {
if let (icon, statusColor) = status.statusIcon(Color.secondary) {
switch status {
case .sndRcvd:
ZStack(alignment: .trailing) {
+31 -4
View File
@@ -13,7 +13,6 @@ struct ChatItemView: View {
@ObservedObject var chat: Chat
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,7 +23,6 @@ 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),
@@ -34,7 +32,6 @@ struct ChatItemView: View {
self.chat = chat
self.chatItem = chatItem
self.maxWidth = maxWidth
_scrollProxy = .init(initialValue: scrollProxy)
_revealed = revealed
_allowMenu = allowMenu
_audioPlayer = audioPlayer
@@ -62,7 +59,37 @@ struct ChatItemView: View {
}
private func framedItemView() -> some View {
FramedItemView(chat: chat, chatItem: chatItem, revealed: $revealed, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
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
)
}
}
File diff suppressed because it is too large Load Diff
@@ -9,7 +9,7 @@
import SwiftUI
struct ComposeFileView: View {
@EnvironmentObject var theme: AppTheme
@Environment(\.colorScheme) var colorScheme
let fileName: String
let cancelFile: (() -> Void)
let cancelEnabled: Bool
@@ -33,7 +33,7 @@ struct ComposeFileView: View {
.padding(.vertical, 1)
.padding(.trailing, 12)
.frame(height: 50)
.background(theme.appColors.sentMessage)
.background(colorScheme == .light ? sentColorLight : sentColorDark)
.frame(maxWidth: .infinity)
.padding(.top, 8)
}
@@ -10,7 +10,7 @@ import SwiftUI
import SimpleXChat
struct ComposeImageView: View {
@EnvironmentObject var theme: AppTheme
@Environment(\.colorScheme) var colorScheme
let images: [String]
let cancelImage: (() -> Void)
let cancelEnabled: Bool
@@ -48,7 +48,7 @@ struct ComposeImageView: View {
}
.padding(.vertical, 1)
.padding(.trailing, 12)
.background(theme.appColors.sentMessage)
.background(colorScheme == .light ? sentColorLight : sentColorDark)
.frame(maxWidth: .infinity)
.padding(.top, 8)
}
@@ -40,7 +40,7 @@ func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) {
}
struct ComposeLinkView: View {
@EnvironmentObject var theme: AppTheme
@Environment(\.colorScheme) var colorScheme
let linkPreview: LinkPreview?
var cancelPreview: (() -> Void)? = nil
let cancelEnabled: Bool
@@ -62,7 +62,7 @@ struct ComposeLinkView: View {
}
.padding(.vertical, 1)
.padding(.trailing, 12)
.background(theme.appColors.sentMessage)
.background(colorScheme == .light ? sentColorLight : sentColorDark)
.frame(maxWidth: .infinity)
.padding(.top, 8)
}
@@ -254,7 +254,6 @@ enum UploadContent: Equatable {
struct ComposeView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
@Binding var composeState: ComposeState
@Binding var keyboardVisible: Bool
@@ -287,6 +286,7 @@ 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)
@@ -354,10 +354,10 @@ struct ComposeView: View {
keyboardVisible: $keyboardVisible,
sendButtonColor: chat.chatInfo.incognito
? .indigo.opacity(colorScheme == .dark ? 1 : 0.7)
: theme.colors.primary
: .accentColor
)
.padding(.trailing, 12)
.background(theme.colors.background)
.background(.background)
.disabled(!chat.userCanSend)
if chat.userIsObserver {
@@ -1066,7 +1066,7 @@ struct ComposeView: View {
} else {
nil
}
let simplexLink = parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false })
let simplexLink = parsedMsgHasSimplexLink(parsedMsg)
return (url, simplexLink)
}
@@ -1106,6 +1106,10 @@ 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: [])
@@ -25,7 +25,7 @@ func voiceMessageTime_(_ time: TimeInterval?) -> String {
struct ComposeVoiceView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.colorScheme) var colorScheme
var recordingFileName: String
@Binding var recordingTime: TimeInterval?
@Binding var recordingState: VoiceMessageRecordingState
@@ -50,7 +50,7 @@ struct ComposeVoiceView: View {
}
.padding(.vertical, 1)
.frame(height: ComposeVoiceView.previewHeight)
.background(theme.appColors.sentMessage)
.background(colorScheme == .light ? sentColorLight : sentColorDark)
.frame(maxWidth: .infinity)
.padding(.top, 8)
}
@@ -80,7 +80,7 @@ struct ComposeVoiceView: View {
Button {
startPlayback()
} label: {
playPauseIcon("play.fill", theme.colors.primary)
playPauseIcon("play.fill")
}
Text(voiceMessageTime_(recordingTime))
case .playing:
@@ -88,7 +88,7 @@ struct ComposeVoiceView: View {
audioPlayer?.pause()
playbackState = .paused
} label: {
playPauseIcon("pause.fill", theme.colors.primary)
playPauseIcon("pause.fill")
}
Text(voiceMessageTime_(playbackTime))
case .paused:
@@ -96,7 +96,7 @@ struct ComposeVoiceView: View {
audioPlayer?.play()
playbackState = .playing
} label: {
playPauseIcon("play.fill", theme.colors.primary)
playPauseIcon("play.fill")
}
Text(voiceMessageTime_(playbackTime))
}
@@ -131,7 +131,7 @@ struct ComposeVoiceView: View {
}
}
private func playPauseIcon(_ image: String, _ color: Color) -> some View {
private func playPauseIcon(_ image: String, _ color: Color = .accentColor) -> some View {
Image(systemName: image)
.resizable()
.aspectRatio(contentMode: .fit)
@@ -162,7 +162,6 @@ struct ComposeVoiceView: View {
}
private struct ProgressBar: View {
@EnvironmentObject var theme: AppTheme
var length: TimeInterval
@Binding var progress: TimeInterval?
@@ -170,7 +169,7 @@ struct ComposeVoiceView: View {
GeometryReader { geometry in
ZStack {
Rectangle()
.fill(theme.colors.primary)
.fill(Color.accentColor)
.frame(width: min(CGFloat((progress ?? TimeInterval(0)) / length) * geometry.size.width, geometry.size.width), height: 4)
.animation(.linear, value: progress)
}
@@ -9,7 +9,7 @@
import SwiftUI
struct ContextInvitingContactMemberView: View {
@EnvironmentObject var theme: AppTheme
@Environment(\.colorScheme) var colorScheme
var body: some View {
HStack {
@@ -20,7 +20,7 @@ struct ContextInvitingContactMemberView: View {
.padding(12)
.frame(minHeight: 50)
.frame(maxWidth: .infinity, alignment: .leading)
.background(theme.appColors.sentMessage)
.background(colorScheme == .light ? sentColorLight : sentColorDark)
.padding(.top, 8)
}
}
@@ -10,7 +10,7 @@ import SwiftUI
import SimpleXChat
struct ContextItemView: View {
@EnvironmentObject var theme: AppTheme
@Environment(\.colorScheme) var colorScheme
@ObservedObject var chat: Chat
let contextItem: ChatItem
let contextIcon: String
@@ -44,7 +44,7 @@ struct ContextItemView: View {
.padding(12)
.frame(minHeight: 50)
.frame(maxWidth: .infinity)
.background(chatItemFrameColor(contextItem, theme))
.background(chatItemFrameColor(contextItem, colorScheme))
.padding(.top, 8)
}
@@ -28,7 +28,6 @@ struct NativeTextEditor: UIViewRepresentable {
func makeUIView(context: Context) -> UITextView {
let field = CustomUITextField(height: _height)
field.backgroundColor = .clear
field.text = text
field.textAlignment = alignment(text)
field.autocapitalizationType = .sentences
@@ -13,7 +13,6 @@ private let liveMsgInterval: UInt64 = 3000_000000
struct SendMessageView: View {
@Binding var composeState: ComposeState
@EnvironmentObject var theme: AppTheme
var sendMessage: (Int?) -> Void
var sendLiveMessage: (() async -> Void)? = nil
var updateLiveMessage: (() async -> Void)? = nil
@@ -248,7 +247,6 @@ struct SendMessageView: View {
}
private struct RecordVoiceMessageButton: View {
@EnvironmentObject var theme: AppTheme
var startVoiceMessageRecording: (() -> Void)?
var finishVoiceMessageRecording: (() -> Void)?
@Binding var holdingVMR: Bool
@@ -258,7 +256,7 @@ struct SendMessageView: View {
var body: some View {
Button(action: {}) {
Image(systemName: "mic.fill")
.foregroundColor(theme.colors.primary)
.foregroundColor(.accentColor)
}
.disabled(disabled)
.frame(width: 29, height: 29)
@@ -325,7 +323,7 @@ struct SendMessageView: View {
Image(systemName: "multiply")
.resizable()
.scaledToFit()
.foregroundColor(theme.colors.primary)
.foregroundColor(.accentColor)
.frame(width: 15, height: 15)
}
.frame(width: 29, height: 29)
@@ -342,7 +340,7 @@ struct SendMessageView: View {
Image(systemName: "bolt.fill")
.resizable()
.scaledToFit()
.foregroundColor(theme.colors.primary)
.foregroundColor(.accentColor)
.frame(width: 20, height: 20)
}
.frame(width: 29, height: 29)
@@ -385,7 +383,7 @@ struct SendMessageView: View {
}
Task {
_ = try? await Task.sleep(nanoseconds: liveMsgInterval)
while await composeState.liveMessage != nil {
while composeState.liveMessage != nil {
await update()
_ = try? await Task.sleep(nanoseconds: liveMsgInterval)
}
@@ -396,7 +394,7 @@ struct SendMessageView: View {
private func finishVoiceMessageRecordingButton() -> some View {
Button(action: { finishVoiceMessageRecording?() }) {
Image(systemName: "stop.fill")
.foregroundColor(theme.colors.primary)
.foregroundColor(.accentColor)
}
.disabled(composeState.inProgress)
.frame(width: 29, height: 29)
@@ -21,7 +21,6 @@ struct AddGroupMembersView: View {
struct AddGroupMembersViewCommon: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
var chat: Chat
@State var groupInfo: GroupInfo
var creatingGroup: Bool = false
@@ -126,7 +125,6 @@ struct AddGroupMembersViewCommon: View {
.onChange(of: selectedContacts) { _ in
searchFocussed = false
}
.modifier(ThemedBackground())
}
private func inviteMembersButton() -> some View {
@@ -178,7 +176,7 @@ struct AddGroupMembersViewCommon: View {
} else {
if checked {
icon = "checkmark.circle.fill"
iconColor = theme.colors.primary
iconColor = .accentColor
} else {
icon = "circle"
iconColor = Color(uiColor: .tertiaryLabel)
@@ -135,7 +135,6 @@ struct GroupChatInfoView: View {
}
}
}
.modifier(ThemedBackground())
.navigationBarHidden(true)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
@@ -211,7 +210,6 @@ struct GroupChatInfoView: View {
private struct MemberRowView: View {
var groupInfo: GroupInfo
@ObservedObject var groupMember: GMember
@EnvironmentObject var theme: AppTheme
var user: Bool = false
@Binding var alert: GroupChatInfoViewAlert?
@@ -284,7 +282,7 @@ struct GroupChatInfoView: View {
Button {
alert = .unblockMemberAlert(mem: member)
} label: {
Label("Unblock member", systemImage: "hand.raised.slash").foregroundColor(theme.colors.primary)
Label("Unblock member", systemImage: "hand.raised.slash").foregroundColor(.accentColor)
}
}
}
@@ -296,7 +294,7 @@ struct GroupChatInfoView: View {
Button {
alert = .unblockForAllAlert(mem: member)
} label: {
Label("Unblock for all", systemImage: "hand.raised.slash").foregroundColor(theme.colors.primary)
Label("Unblock for all", systemImage: "hand.raised.slash").foregroundColor(.accentColor)
}
} else {
Button {
@@ -335,7 +333,6 @@ struct GroupChatInfoView: View {
creatingGroup: false
)
.navigationBarTitle("Group link")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
if groupLink == nil {
@@ -353,7 +350,6 @@ struct GroupChatInfoView: View {
groupProfile: groupInfo.groupProfile
)
.navigationBarTitle("Group profile")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
Label("Edit group profile", systemImage: "pencil")
@@ -368,7 +364,6 @@ struct GroupChatInfoView: View {
welcomeText: groupInfo.groupProfile.description ?? ""
)
.navigationTitle("Welcome message")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
groupInfo.groupProfile.description == nil
@@ -523,7 +518,6 @@ func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bo
creatingGroup: creatingGroup
)
.navigationBarTitle("Group preferences")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
if creatingGroup {
@@ -133,7 +133,6 @@ struct GroupLinkView: View {
shouldCreate = false
}
}
.modifier(ThemedBackground())
}
private func createGroupLink() {
@@ -247,7 +247,6 @@ struct GroupMemberInfoView: View {
ProgressView().scaleEffect(2)
}
}
.modifier(ThemedBackground())
}
func connectViaAddressButton(_ contactLink: String) -> some View {
@@ -375,7 +374,6 @@ struct GroupMemberInfoView: View {
)
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("Security code")
.modifier(ThemedBackground())
} label: {
Label(
member.verified ? "View security code" : "Verify security code",
@@ -0,0 +1,270 @@
//
// 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
// 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
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.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()
}
}
@@ -66,7 +66,6 @@ struct VerifyCodeView: View {
ScanCodeView(connectionVerified: $connectionVerified, verify: verify)
.navigationBarTitleDisplayMode(.large)
.navigationTitle("Scan code")
.modifier(ThemedBackground())
} label: {
Label("Scan code", systemImage: "qrcode")
}
@@ -26,7 +26,6 @@ private let rowHeights: [DynamicTypeSize: CGFloat] = [
struct ChatListNavLink: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
@ObservedObject var chat: Chat
@State private var showContactRequestDialog = false
@@ -225,7 +224,7 @@ struct ChatListNavLink: View {
} label: {
Label("Join", systemImage: chat.chatInfo.incognito ? "theatermasks" : "ipad.and.arrow.forward")
}
.tint(chat.chatInfo.incognito ? .indigo : theme.colors.primary)
.tint(chat.chatInfo.incognito ? .indigo : .accentColor)
}
@ViewBuilder private func markReadButton() -> some View {
@@ -235,14 +234,14 @@ struct ChatListNavLink: View {
} label: {
Label("Read", systemImage: "checkmark")
}
.tint(theme.colors.primary)
.tint(Color.accentColor)
} else {
Button {
Task { await markChatUnread(chat) }
} label: {
Label("Unread", systemImage: "circlebadge.fill")
}
.tint(theme.colors.primary)
.tint(Color.accentColor)
}
}
@@ -307,7 +306,7 @@ struct ChatListNavLink: View {
Button {
Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) }
} label: { Label("Accept", systemImage: "checkmark") }
.tint(theme.colors.primary)
.tint(.accentColor)
Button {
Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) }
} label: {
@@ -347,7 +346,7 @@ struct ChatListNavLink: View {
} label: {
Label("Name", systemImage: "pencil")
}
.tint(theme.colors.primary)
.tint(.accentColor)
}
.frame(height: rowHeights[dynamicTypeSize])
.appSheet(isPresented: $showContactConnectionInfo) {
@@ -355,7 +354,6 @@ struct ChatListNavLink: View {
if case let .contactConnection(contactConnection) = chat.chatInfo {
ContactConnectionInfo(contactConnection: contactConnection)
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
.modifier(ThemedBackground())
}
}
}
@@ -11,7 +11,6 @@ import SimpleXChat
struct ChatListView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Binding var showSettings: Bool
@State private var searchMode = false
@FocusState private var searchFocussed
@@ -87,7 +86,6 @@ struct ChatListView: View {
))
}
.listStyle(.plain)
.background(theme.colors.background)
.navigationBarTitleDisplayMode(.inline)
.navigationBarHidden(searchMode)
.toolbar {
@@ -117,9 +115,7 @@ struct ChatListView: View {
HStack(spacing: 4) {
Text("Chats")
.font(.headline)
if chatModel.chats.count > 0 {
toggleFilterButton()
}
SubsStatusIndicator()
}
.frame(maxWidth: .infinity, alignment: .center)
}
@@ -133,15 +129,6 @@ 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 {
@@ -156,14 +143,12 @@ struct ChatListView: View {
searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink
)
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
.frame(maxWidth: .infinity)
}
ForEach(cs, id: \.viewId) { chat in
ChatListNavLink(chat: chat)
.padding(.trailing, -16)
.disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id))
.listRowBackground(Color.clear)
}
.offset(x: -8)
}
@@ -183,7 +168,7 @@ struct ChatListView: View {
private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View {
Circle()
.frame(width: size, height: size)
.foregroundColor(theme.colors.primary)
.foregroundColor(.accentColor)
}
private func onboardingButtons() -> some View {
@@ -194,7 +179,7 @@ struct ChatListView: View {
p.addLine(to: CGPoint(x: 0, y: 10))
p.addLine(to: CGPoint(x: 8, y: 0))
}
.fill(theme.colors.primary)
.fill(Color.accentColor)
.frame(width: 20, height: 10)
.padding(.trailing, 12)
@@ -217,16 +202,14 @@ struct ChatListView: View {
.padding(.vertical, 10)
.padding(.horizontal, 20)
}
.background(theme.colors.primary)
.background(Color.accentColor)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
@ViewBuilder private func chatView() -> some View {
if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) {
ChatView(chat: chat).onAppear {
loadChat(chat: chat)
}
ChatView(chat: chat)
}
}
@@ -278,18 +261,86 @@ 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
@Binding var searchMode: Bool
@FocusState.Binding var searchFocussed: Bool
@Binding var searchText: String
@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) {
@@ -306,26 +357,6 @@ 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))
@@ -335,19 +366,17 @@ struct ChatListSearchBar: View {
if searchFocussed {
Text("Cancel")
.foregroundColor(theme.colors.primary)
.foregroundColor(.accentColor)
.onTapGesture {
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 }
}
@@ -381,6 +410,21 @@ 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 ? .accentColor : .secondary)
.frame(width: showUnreadAndFavorites ? 22 : 16, height: showUnreadAndFavorites ? 22 : 16)
.onTapGesture {
showUnreadAndFavorites = !showUnreadAndFavorites
}
}
}
private func connect(_ link: String) {
planAndConnect(
link,
@@ -11,7 +11,6 @@ import SimpleXChat
struct ChatPreviewView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
@Binding var progressByTimeout: Bool
@State var deleting: Bool = false
@@ -95,7 +94,7 @@ struct ChatPreviewView: View {
case let .group(groupInfo):
let v = previewTitle(t)
switch (groupInfo.membership.memberStatus) {
case .memInvited: v.foregroundColor(deleting ? .secondary : chat.chatInfo.incognito ? .indigo : theme.colors.primary)
case .memInvited: v.foregroundColor(deleting ? .secondary : chat.chatInfo.incognito ? .indigo : .accentColor)
case .memAccepted: v.foregroundColor(.secondary)
default: if deleting { v.foregroundColor(.secondary) } else { v }
}
@@ -134,7 +133,7 @@ struct ChatPreviewView: View {
.foregroundColor(.white)
.padding(.horizontal, 4)
.frame(minWidth: 18, minHeight: 18)
.background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? theme.colors.primary : theme.colors.secondary)
.background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? Color.accentColor : Color.secondary)
.cornerRadius(10)
} else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local {
Image(systemName: "speaker.slash.fill")
@@ -152,7 +151,7 @@ struct ChatPreviewView: View {
private func messageDraft(_ draft: ComposeState) -> Text {
let msg = draft.message
return image("rectangle.and.pencil.and.ellipsis", color: theme.colors.primary)
return image("rectangle.and.pencil.and.ellipsis", color: .accentColor)
+ attachment()
+ messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true, showSecrets: false)
@@ -207,7 +206,7 @@ struct ChatPreviewView: View {
case let .direct(contact):
if contact.activeConn == nil && contact.profile.contactLink != nil {
chatPreviewInfoText("Tap to Connect")
.foregroundColor(theme.colors.primary)
.foregroundColor(.accentColor)
} else if !contact.ready && contact.activeConn != nil {
if contact.nextSendGrpInv {
chatPreviewInfoText("send direct message")
@@ -11,7 +11,6 @@ import SimpleXChat
struct ContactConnectionInfo: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@State var contactConnection: PendingContactConnection
@State private var alert: CCInfoAlert?
@@ -83,7 +82,6 @@ struct ContactConnectionInfo: View {
}
}
}
.modifier(ThemedBackground())
if #available(iOS 16, *) {
v
} else {
@@ -151,7 +149,7 @@ struct ContactConnectionInfo: View {
HStack(spacing: 6) {
Text("Incognito")
Image(systemName: "info.circle")
.foregroundColor(theme.colors.primary)
.foregroundColor(.accentColor)
.font(.system(size: 14))
}
.onTapGesture {
@@ -180,7 +178,6 @@ private func oneTimeLinkLearnMoreButton() -> some View {
NavigationLink {
AddContactLearnMore(showTitle: false)
.navigationTitle("One-time invitation link")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
settingsRow("info.circle") {
@@ -11,7 +11,6 @@ import SimpleXChat
struct ContactRequestView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
var contactRequest: UserContactRequest
@ObservedObject var chat: Chat
@@ -24,7 +23,7 @@ struct ContactRequestView: View {
Text(contactRequest.chatViewName)
.font(.title3)
.fontWeight(.bold)
.foregroundColor(theme.colors.primary)
.foregroundColor(.accentColor)
.padding(.leading, 8)
.frame(alignment: .topLeading)
Spacer()
@@ -0,0 +1,741 @@
//
// 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)
.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)
} 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)
} 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 servers 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("\(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")
} 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)
} 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")
indentedInfoRow("AUTH", numOrDash(stats._sentAuthErrs))
indentedInfoRow("QUOTA", numOrDash(stats._sentQuotaErrs))
indentedInfoRow("expired", numOrDash(stats._sentExpiredErrs))
indentedInfoRow("other", numOrDash(stats._sentOtherErrs))
}
Section("Received messages") {
infoRow("Received total", numOrDash(stats._recvMsgs))
Text("Receive errors")
indentedInfoRow("duplicates", numOrDash(stats._recvDuplicates))
indentedInfoRow("decryption errors", numOrDash(stats._recvCryptoErrs))
indentedInfoRow("other errors", numOrDash(stats._recvErrs))
infoRowTwoValues("Acknowledged", "attempts", stats._ackMsgs, stats._ackAttempts)
Text("Acknowledgement errors")
indentedInfoRow("NO_MSG errors", numOrDash(stats._ackNoMsgErrs))
indentedInfoRow("other errors", numOrDash(stats._ackOtherErrs))
}
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("Subscription results 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(" / ").font(.caption2) + Text(title2).font(.caption2)
Spacer()
Group {
if value == 0 && value2 == 0 {
Text("-")
} else {
Text(numOrDash(value)) + Text(" / ").font(.caption2) + Text(numOrDash(value2)).font(.caption2)
}
}
.foregroundStyle(.secondary)
}
}
private func indentedInfoRow(_ title: LocalizedStringKey, _ value: String) -> some View {
HStack {
Text(title)
.padding(.leading, 24)
Spacer()
Text(value)
.foregroundStyle(.secondary)
}
}
struct XFTPServerSummaryView: View {
var summary: XFTPServerSummary
var statsStartedAt: Date
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")
} 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)
} 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")
indentedInfoRow("AUTH", numOrDash(stats._downloadAuthErrs))
indentedInfoRow("other", numOrDash(stats._downloadErrs))
} header: {
Text("Downloaded files")
} footer: {
Text("Starting from \(localTimestamp(statsStartedAt)).")
}
}
}
}
#Preview {
ServersSummaryView()
}
@@ -6,11 +6,13 @@
import SwiftUI
import SimpleXChat
private let fillColorDark = Color(uiColor: UIColor(red: 0.11, green: 0.11, blue: 0.11, alpha: 255))
private let fillColorLight = Color(uiColor: UIColor(red: 0.99, green: 0.99, blue: 0.99, alpha: 255))
struct UserPicker: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
@Environment(\.scenePhase) var scenePhase
@EnvironmentObject var theme: AppTheme
@Binding var showSettings: Bool
@Binding var showConnectDesktop: Bool
@Binding var userPickerVisible: Bool
@@ -19,6 +21,9 @@ struct UserPicker: View {
private let menuButtonHeight: CGFloat = 68
@State var chatViewNameWidth: CGFloat = 0
var fillColor: Color {
colorScheme == .dark ? fillColorDark : fillColorLight
}
var body: some View {
VStack {
@@ -77,7 +82,7 @@ struct UserPicker: View {
.clipShape(RoundedRectangle(cornerRadius: 16))
.background(
Rectangle()
.fill(theme.colors.surface)
.fill(fillColor)
.cornerRadius(16)
.shadow(color: .black.opacity(0.12), radius: 24, x: 0, y: 0)
)
@@ -132,7 +137,7 @@ struct UserPicker: View {
if user.activeUser {
Image(systemName: "checkmark")
} else if u.unreadCount > 0 {
unreadCounter(u.unreadCount, color: user.showNtfs ? theme.colors.primary : theme.colors.secondary)
unreadCounter(u.unreadCount, color: user.showNtfs ? .accentColor : .secondary)
} else if !user.showNtfs {
Image(systemName: "speaker.slash")
}
@@ -140,7 +145,7 @@ struct UserPicker: View {
.padding(.trailing)
.padding([.leading, .vertical], 12)
})
.buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill)))
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
}
private func menuButton(_ title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View {
@@ -157,7 +162,7 @@ struct UserPicker: View {
.padding(.vertical, 22)
.frame(height: menuButtonHeight)
}
.buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill)))
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
}
}
@@ -118,7 +118,6 @@ struct DatabaseView: View {
NavigationLink {
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false)
.navigationTitle("Database passphrase")
.modifier(ThemedBackground())
} label: {
Text("Database passphrase")
}
@@ -145,7 +144,6 @@ struct DatabaseView: View {
NavigationLink {
ChatArchiveView(archiveName: archiveName)
.navigationTitle(title)
.modifier(ThemedBackground())
} label: {
Text(title)
}
@@ -357,7 +355,6 @@ struct DatabaseView: View {
Task {
do {
try await apiDeleteStorage()
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
do {
let config = ArchiveConfig(archivePath: archivePath.path)
let archiveErrors = try await apiImportArchive(config: config)
@@ -189,7 +189,6 @@ struct MigrateToAppGroupView: View {
Task {
do {
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
try await apiExportArchive(config: config)
await MainActor.run { setV3DBMigration(.exported) }
} catch let error {
@@ -232,7 +231,6 @@ func exportChatArchive(_ storagePath: URL? = nil) async throws -> URL {
if !ChatModel.shared.chatDbChanged {
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
}
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
try await apiExportArchive(config: config)
if storagePath == nil {
deleteOldArchive()
@@ -10,7 +10,7 @@ import SwiftUI
import SimpleXChat
struct ChatInfoImage: View {
@EnvironmentObject var theme: AppTheme
@Environment(\.colorScheme) var colorScheme
@ObservedObject var chat: Chat
var size: CGFloat
var color = Color(uiColor: .tertiarySystemGroupedBackground)
@@ -24,7 +24,8 @@ struct ChatInfoImage: View {
case .contactRequest: iconName = "person.crop.circle.fill"
default: iconName = "circle.fill"
}
let iconColor = if case .local = chat.chatInfo { theme.appColors.primaryVariant2 } else { color }
let notesColor = colorScheme == .light ? notesChatColorLight : notesChatColorDark
let iconColor = if case .local = chat.chatInfo { notesColor } else { color }
return ProfileImage(
imageStr: chat.chatInfo.image,
iconName: iconName,
@@ -1,91 +0,0 @@
//
// ChatWallpaper.swift
// SimpleX (iOS)
//
// Created by Avently on 14.06.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import Foundation
import SwiftUI
import SimpleXChat
struct ChatViewBackground: ViewModifier {
@EnvironmentObject var theme: AppTheme
var image: Image
var imageType: WallpaperType
var background: Color
var tint: Color
func body(content: Content) -> some View {
content.background(
Canvas { context, size in
var image = context.resolve(image)
image.shading = .color(tint)
let rect = CGRectMake(0, 0, size.width, size.height)
func repeatDraw(_ imageScale: CGFloat) {
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)
context.draw(image, in: rect, style: FillStyle())
}
}
}
context.fill(Path(rect), with: .color(background))
switch imageType {
case let WallpaperType.Preset(filename, scale): repeatDraw(CGFloat((scale ?? 1) * (PresetWallpaper.from(filename)?.scale ?? 1)))
case let WallpaperType.Image(_, scale, scaleType):
let scaleType = scaleType ?? WallpaperScaleType.fill
switch scaleType {
case WallpaperScaleType.repeat: repeatDraw(CGFloat(scale ?? 1))
case WallpaperScaleType.fill: fallthrough
case WallpaperScaleType.fit:
// let scale = scaleType.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: ()
}
}
)
}
}
extension PresetWallpaper {
public func toType(_ base: DefaultTheme, _ scale: Float? = nil) -> WallpaperType {
WallpaperType.Preset(
filename,
scale ?? themeOverridesDefault.get().first { $0.wallpaper != nil && $0.wallpaper!.preset == filename && $0.base == base }?.wallpaper?.scale ?? 1
)
}
}
@@ -1,116 +0,0 @@
//
// 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")
// }
// }
}
}
@@ -16,7 +16,6 @@ struct ProfileImage: View {
var iconName: String = "person.crop.circle.fill"
var size: CGFloat
var color = Color(uiColor: .tertiarySystemGroupedBackground)
var backgroundColor: Color? = nil
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
var body: some View {
@@ -29,7 +28,6 @@ struct ProfileImage: View {
.resizable()
.foregroundColor(color)
.frame(width: size, height: size)
.background(backgroundColor != nil ? backgroundColor! : .clear)
}
}
}
@@ -0,0 +1,26 @@
//
// TranslateSheet.swift
// SimpleX (iOS)
//
// Created by Levitating Pineapple on 03/07/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
import Translation
extension View {
@ViewBuilder func translateSheet(text: Binding<String?>) -> some View {
if #available(iOS 17.4, *) {
translationPresentation(
isPresented: Binding(
get: { text.wrappedValue != nil },
set: { if !$0 { text.wrappedValue = nil } }
),
text: text.wrappedValue ?? String()
)
} else {
self
}
}
}
@@ -1,19 +0,0 @@
//
// ViewModifiers.swift
// SimpleX (iOS)
//
// Created by Avently on 12.06.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
extension View {
@ViewBuilder func `if`<Content: View>(_ condition: @autoclosure () -> Bool, transform: (Self) -> Content) -> some View {
if condition() {
transform(self)
} else {
self
}
}
}
@@ -140,7 +140,7 @@ struct PasscodeEntry: View {
ZStack {
Circle()
.frame(width: h, height: h)
.foregroundColor(AppTheme.shared.colors.background)
.foregroundColor(Color(uiColor: .systemBackground))
label()
}
}
@@ -29,7 +29,7 @@ struct PasscodeView: View {
}
.padding(.horizontal, 40)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(AppTheme.shared.colors.background)
.background(Color(uiColor: .systemBackground))
}
private func verticalPasscodeView(_ g: GeometryProxy) -> some View {
@@ -53,7 +53,6 @@ private enum MigrateFromDeviceViewAlert: Identifiable {
struct MigrateFromDevice: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@Binding var showSettings: Bool
@Binding var showProgressOnSettings: Bool
@@ -216,7 +215,7 @@ struct MigrateFromDevice: View {
Section {
Button(action: { migrationState = .archiving }) {
settingsRow("tray.and.arrow.up") {
Text("Archive and upload").foregroundColor(theme.colors.primary)
Text("Archive and upload").foregroundColor(.accentColor)
}
}
} header: {
@@ -250,7 +249,7 @@ struct MigrateFromDevice: View {
}
}
let ratio = Float(uploadedBytes) / Float(totalBytes)
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: uploadedBytes, countStyle: .binary)) uploaded", theme.colors.primary)
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: uploadedBytes, countStyle: .binary)) uploaded")
}
.onAppear {
startUploading(totalBytes, archivePath)
@@ -264,7 +263,7 @@ struct MigrateFromDevice: View {
migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil)
}) {
settingsRow("tray.and.arrow.up") {
Text("Repeat upload").foregroundColor(theme.colors.primary)
Text("Repeat upload").foregroundColor(.accentColor)
}
}
} header: {
@@ -300,7 +299,7 @@ struct MigrateFromDevice: View {
}
Button(action: { finishMigration(fileId, ctrl) }) {
settingsRow("checkmark") {
Text("Finalize migration").foregroundColor(theme.colors.primary)
Text("Finalize migration").foregroundColor(.accentColor)
}
}
} footer: {
@@ -341,7 +340,7 @@ struct MigrateFromDevice: View {
}
Button(action: { alert = .deleteChat() }) {
settingsRow("trash.fill") {
Text("Delete database from this device").foregroundColor(theme.colors.primary)
Text("Delete database from this device").foregroundColor(.accentColor)
}
}
} header: {
@@ -380,7 +379,7 @@ struct MigrateFromDevice: View {
.truncationMode(.middle)
}
static func largeProgressView(_ value: Float, _ title: String, _ description: LocalizedStringKey, _ primaryColor: Color) -> some View {
static func largeProgressView(_ value: Float, _ title: String, _ description: LocalizedStringKey) -> some View {
ZStack {
VStack {
Text(description)
@@ -390,7 +389,7 @@ struct MigrateFromDevice: View {
Text(title)
.font(.system(size: 54))
.bold()
.foregroundColor(primaryColor)
.foregroundColor(.accentColor)
Text(description)
.font(.title3)
@@ -399,7 +398,7 @@ struct MigrateFromDevice: View {
Circle()
.trim(from: 0, to: CGFloat(value))
.stroke(
primaryColor,
Color.accentColor,
style: StrokeStyle(lineWidth: 27)
)
.rotationEffect(.degrees(180))
@@ -91,7 +91,6 @@ private enum MigrateToDeviceViewAlert: Identifiable {
struct MigrateToDevice: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@Binding var migrationState: MigrationToState?
@@ -244,7 +243,7 @@ struct MigrateToDevice: View {
}
}
let ratio = Float(downloadedBytes) / Float(max(totalBytes, 1))
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: downloadedBytes, countStyle: .binary)) downloaded", theme.colors.primary)
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: downloadedBytes, countStyle: .binary)) downloaded")
}
}
@@ -256,7 +255,7 @@ struct MigrateToDevice: View {
migrationState = .linkDownloading(link: link)
}) {
settingsRow("tray.and.arrow.down") {
Text("Repeat download").foregroundColor(theme.colors.primary)
Text("Repeat download").foregroundColor(.accentColor)
}
}
} header: {
@@ -294,7 +293,7 @@ struct MigrateToDevice: View {
migrationState = .archiveImport(archivePath: archivePath)
}) {
settingsRow("square.and.arrow.down") {
Text("Repeat import").foregroundColor(theme.colors.primary)
Text("Repeat import").foregroundColor(.accentColor)
}
}
} header: {
@@ -335,7 +334,7 @@ struct MigrateToDevice: View {
migrationState = .migration(passphrase: passphrase, confirmation: confirmation, useKeychain: useKeychain)
}) {
settingsRow("square.and.arrow.down") {
Text(button).foregroundColor(theme.colors.primary)
Text(button).foregroundColor(.accentColor)
}
}
} else {
@@ -365,7 +364,6 @@ struct MigrateToDevice: View {
}
struct OnionView: View {
@EnvironmentObject var theme: AppTheme
@State var appSettings: AppSettings
@State private var onionHosts: OnionHosts = .no
var finishMigration: (AppSettings) -> Void
@@ -383,7 +381,7 @@ struct MigrateToDevice: View {
finishMigration(appSettings)
}) {
settingsRow("checkmark") {
Text("Apply").foregroundColor(theme.colors.primary)
Text("Apply").foregroundColor(.accentColor)
}
}
} header: {
@@ -477,7 +475,6 @@ struct MigrateToDevice: View {
chatInitControllerRemovingDatabases()
}
try await apiDeleteStorage()
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
do {
let config = ArchiveConfig(archivePath: archivePath)
let archiveErrors = try await apiImportArchive(config: config)
@@ -30,7 +30,6 @@ struct AddContactLearnMore: View {
}
.listRowBackground(Color.clear)
}
.modifier(ThemedBackground())
}
}
@@ -11,7 +11,6 @@ import SimpleXChat
struct AddGroupView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
@State private var chat: Chat?
@@ -96,7 +95,7 @@ struct AddGroupView: View {
Section {
groupNameTextField()
Button(action: createGroup) {
settingsRow("checkmark", color: theme.colors.primary) { Text("Create group") }
settingsRow("checkmark", color: .accentColor) { Text("Create group") }
}
.disabled(!canCreateProfile())
IncognitoToggle(incognitoEnabled: $incognitoDefault)
@@ -145,7 +144,6 @@ struct AddGroupView: View {
profile.image = nil
}
}
.modifier(ThemedBackground())
}
func groupNameTextField() -> some View {
@@ -10,6 +10,7 @@ import SwiftUI
enum NewChatMenuOption: Identifiable {
case newContact
case scanPaste
case newGroup
var id: Self { self }
@@ -25,6 +26,11 @@ struct NewChatMenuButton: View {
} label: {
Text("Add contact")
}
Button {
newChatMenuOption = .scanPaste
} label: {
Text("Scan / Paste link")
}
Button {
newChatMenuOption = .newGroup
} label: {
@@ -39,6 +45,7 @@ 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()
}
}
+12 -18
View File
@@ -11,14 +11,9 @@ import SimpleXChat
import CodeScanner
import AVFoundation
enum SomeAlert: Identifiable {
case someAlert(alert: Alert, id: String)
var id: String {
switch self {
case let .someAlert(_, id): return id
}
}
struct SomeAlert: Identifiable {
var alert: Alert
var id: String
}
private enum NewChatViewAlert: Identifiable {
@@ -94,7 +89,7 @@ struct NewChatView: View {
.background(
// Rectangle is needed for swipe gesture to work on mostly empty views (creatingLinkProgressView and retryButton)
Rectangle()
.fill(AppTheme.shared.colors.background)
.fill(Color(uiColor: .systemGroupedBackground))
)
.animation(.easeInOut(duration: 0.3333), value: selection)
.gesture(DragGesture(minimumDistance: 20.0, coordinateSpace: .local)
@@ -113,7 +108,7 @@ struct NewChatView: View {
}
)
}
.modifier(ThemedBackground())
.background(Color(.systemGroupedBackground))
.onChange(of: invitationUsed) { used in
if used && !(m.showingInvitation?.connChatUsed ?? true) {
m.markShowingInvitationUsed()
@@ -142,8 +137,8 @@ struct NewChatView: View {
switch(a) {
case let .planAndConnectAlert(alert):
return planAndConnectAlert(alert, dismiss: true, cleanup: { pastedLink = "" })
case let .newChatSomeAlert(.someAlert(alert, _)):
return alert
case let .newChatSomeAlert(a):
return a.alert
}
}
}
@@ -181,7 +176,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"))
}
}
}
@@ -315,7 +310,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 +333,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"
))
@@ -488,7 +483,6 @@ func strHasSingleSimplexLink(_ str: String) -> FormattedText? {
}
struct IncognitoToggle: View {
@EnvironmentObject var theme: AppTheme
@Binding var incognitoEnabled: Bool
@State private var showIncognitoSheet = false
@@ -502,7 +496,7 @@ struct IncognitoToggle: View {
HStack(spacing: 6) {
Text("Incognito")
Image(systemName: "info.circle")
.foregroundColor(theme.colors.primary)
.foregroundColor(.accentColor)
.font(.system(size: 14))
}
.onTapGesture {
@@ -66,7 +66,6 @@ struct CreateProfile: View {
}
}
.navigationTitle("Create your profile")
.modifier(ThemedBackground())
.alert(item: $alert) { a in userProfileAlert(a, $displayName) }
.onAppear() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
@@ -79,7 +78,6 @@ struct CreateProfile: View {
struct CreateFirstProfile: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss
@State private var displayName: String = ""
@FocusState private var focusDisplayName
@@ -91,9 +89,9 @@ struct CreateFirstProfile: View {
.font(.largeTitle)
.bold()
Text("Your profile, contacts and delivered messages are stored on your device.")
.foregroundColor(theme.colors.secondary)
.foregroundColor(.secondary)
Text("The profile is only shared with your contacts.")
.foregroundColor(theme.colors.secondary)
.foregroundColor(.secondary)
.padding(.bottom)
}
.padding(.bottom)
@@ -44,7 +44,6 @@ struct HowItWorks: View {
.lineLimit(10)
.padding()
.frame(maxHeight: .infinity, alignment: .top)
.modifier(ThemedBackground())
}
}
@@ -78,7 +78,6 @@ struct SetNotificationsMode: View {
}
struct NtfModeSelector: View {
@EnvironmentObject var theme: AppTheme
var mode: NotificationsMode
@Binding var selection: NotificationsMode
@State private var tapped = false
@@ -88,7 +87,7 @@ struct NtfModeSelector: View {
VStack(alignment: .leading, spacing: 4) {
Text(mode.label)
.font(.headline)
.foregroundColor(selection == mode ? theme.colors.primary : theme.colors.secondary)
.foregroundColor(selection == mode ? .accentColor : .secondary)
Text(ntfModeDescription(mode))
.lineLimit(10)
.font(.subheadline)
@@ -96,11 +95,11 @@ struct NtfModeSelector: View {
.padding(12)
}
.frame(maxWidth: .infinity, alignment: .leading)
.background(tapped ? Color(uiColor: .secondarySystemFill) : theme.colors.background)
.background(Color(uiColor: tapped ? .secondarySystemFill : .systemBackground))
.clipShape(RoundedRectangle(cornerRadius: 18))
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(selection == mode ? theme.colors.primary : Color(uiColor: .secondarySystemFill), lineWidth: 2)
.stroke(selection == mode ? Color.accentColor : Color(uiColor: .secondarySystemFill), lineWidth: 2)
)
._onButtonGesture { down in
tapped = down
@@ -79,7 +79,7 @@ struct SimpleXInfo: View {
MigrateToDevice(migrationState: $m.migrationState)
}
.navigationTitle("Migrate here")
.modifier(ThemedBackground())
.background(colorScheme == .light ? Color(uiColor: .tertiarySystemGroupedBackground) : .clear)
}
}
.sheet(isPresented: $showHowItWorks) {
@@ -178,7 +178,6 @@ struct ConnectDesktopView: View {
}
}
.navigationTitle("Connect to desktop")
.modifier(ThemedBackground())
}
private func connectingDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> some View {
@@ -203,7 +202,6 @@ struct ConnectDesktopView: View {
ProgressView().scaleEffect(2)
}
.modifier(ThemedBackground())
}
private func searchingDesktopView() -> some View {
@@ -221,7 +219,6 @@ struct ConnectDesktopView: View {
}
}
.navigationTitle("Connecting to desktop")
.modifier(ThemedBackground())
}
@ViewBuilder private func foundDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo, _ compatible: Bool) -> some View {
@@ -249,7 +246,6 @@ struct ConnectDesktopView: View {
}
}
.navigationTitle("Found desktop")
.modifier(ThemedBackground())
if compatible && connectRemoteViaMulticastAuto {
v.onAppear { confirmKnownDesktop(rc) }
@@ -279,7 +275,6 @@ struct ConnectDesktopView: View {
}
}
.navigationTitle("Verify connection")
.modifier(ThemedBackground())
}
private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text {
@@ -320,7 +315,6 @@ struct ConnectDesktopView: View {
}
}
.navigationTitle("Connected to desktop")
.modifier(ThemedBackground())
}
private func sessionCodeText(_ code: String) -> some View {
@@ -339,7 +333,7 @@ struct ConnectDesktopView: View {
}
}
}
private func scanDesctopAddressView() -> some View {
Section("Scan QR code from desktop") {
ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processDesktopQRCode, scanMode: .oncePerCode)
@@ -395,7 +389,6 @@ struct ConnectDesktopView: View {
}
}
.navigationTitle("Linked desktops")
.modifier(ThemedBackground())
}
private func remoteCtrlView(_ rc: RemoteCtrlInfo) -> some View {
-2
View File
@@ -106,7 +106,6 @@ struct TerminalView: View {
}
.navigationViewStyle(.stack)
.navigationTitle("Chat console")
.modifier(ThemedBackground())
}
func scrollToBottom(_ proxy: ScrollViewProxy, animation: Animation = .default) {
@@ -131,7 +130,6 @@ struct TerminalView: View {
}
}
.onDisappear { terminalItem = nil }
.modifier(ThemedBackground())
}
func consoleSendMessage() {
@@ -44,11 +44,6 @@ extension AppSettings {
if let val = androidCallOnLockScreen { def.setValue(val.rawValue, forKey: ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN) }
if let val = iosCallKitEnabled { callKitEnabledGroupDefault.set(val) }
if let val = iosCallKitCallsInRecents { def.setValue(val, forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS) }
if let val = uiProfileImageCornerRadius { def.setValue(val, forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) }
if let val = uiColorScheme { def.setValue(val, forKey: DEFAULT_CURRENT_THEME) }
if let val = uiDarkColorScheme { def.setValue(val, forKey: DEFAULT_SYSTEM_DARK_THEME) }
if let val = uiCurrentThemeIds { def.setValue(val, forKey: DEFAULT_CURRENT_THEME_IDS) }
if let val = uiThemes { def.setValue(val.skipDuplicates(), forKey: DEFAULT_THEME_OVERRIDES) }
}
public static var current: AppSettings {
@@ -74,11 +69,6 @@ extension AppSettings {
c.androidCallOnLockScreen = AppSettingsLockScreenCalls(rawValue: def.string(forKey: ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN)!)
c.iosCallKitEnabled = callKitEnabledGroupDefault.get()
c.iosCallKitCallsInRecents = def.bool(forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS)
c.uiProfileImageCornerRadius = def.float(forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS)
c.uiColorScheme = currentThemeDefault.get()
c.uiDarkColorScheme = systemDarkThemeDefault.get()
c.uiCurrentThemeIds = currentThemeIdsDefault.get()
c.uiThemes = themeOverridesDefault.get()
return c
}
}
@@ -7,17 +7,12 @@
//
import SwiftUI
import SimpleXChat
let defaultAccentColor = CGColor.init(red: 0, green: 0.533, blue: 1, alpha: 1)
//let interfaceStyles: [UIUserInterfaceStyle] = [.unspecified, .light, .dark]
let interfaceStyles: [UIUserInterfaceStyle] = [.unspecified, .light, .dark]
let colorModesLocalized: [LocalizedStringKey] = ["System", "Light", "Dark"]
let colorModesNames: [DefaultThemeMode?] = [nil, DefaultThemeMode.light, DefaultThemeMode.dark]
let darkThemesLocalized: [LocalizedStringKey] = ["Dark", "SimpleX", "Black"]
let darkThemesNames: [String] = [DefaultTheme.DARK.themeName, DefaultTheme.SIMPLEX.themeName, DefaultTheme.BLACK.themeName]
let interfaceStyleNames: [LocalizedStringKey] = ["System", "Light", "Dark"]
let appSettingsURL = URL(string: UIApplication.openSettingsURLString)!
@@ -27,11 +22,7 @@ struct AppearanceSettings: View {
@EnvironmentObject var sceneDelegate: SceneDelegate
@State private var iconLightTapped = false
@State private var iconDarkTapped = false
//@State private var userInterfaceStyle = getUserInterfaceStyleDefault()
@State private var colorMode: DefaultThemeMode? = {
if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME { nil as DefaultThemeMode? } else { CurrentColors.base.mode }
}()
@State private var darkModeTheme: String = UserDefaults.standard.string(forKey: DEFAULT_SYSTEM_DARK_THEME) ?? DefaultTheme.SIMPLEX.themeName
@State private var userInterfaceStyle = getUserInterfaceStyleDefault()
@State private var uiTintColor = getUIAccentColorDefault()
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileImageCornerRadius = defaultProfileImageCorner
@@ -74,15 +65,9 @@ struct AppearanceSettings: View {
}
Section {
Picker("Color mode", selection: $colorMode) {
ForEach(Array(colorModesNames.enumerated()), id: \.element) { index, mode in
Text(colorModesLocalized[index])
}
}
.frame(height: 36)
Picker("Dark mode colors", selection: $darkModeTheme) {
ForEach(Array(darkThemesNames.enumerated()), id: \.element) { index, darkTheme in
Text(darkThemesLocalized[index])
Picker("Theme", selection: $userInterfaceStyle) {
ForEach(interfaceStyles, id: \.self) { style in
Text(interfaceStyleNames[interfaceStyles.firstIndex(of: style) ?? 0])
}
}
.frame(height: 36)
@@ -97,24 +82,9 @@ struct AppearanceSettings: View {
Text("Reset colors").font(.callout)
}
}
.onChange(of: colorMode) { mode in
guard let mode else {
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
return
}
if case DefaultThemeMode.light = mode {
ThemeManager.applyTheme(DefaultTheme.LIGHT.themeName)
} else if case DefaultThemeMode.dark = mode {
ThemeManager.applyTheme(systemDarkThemeDefault.get())
}
}
.onChange(of: darkModeTheme) { darkTheme in
ThemeManager.changeDarkTheme(darkTheme)
if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME {
ThemeManager.applyTheme(currentThemeDefault.get())
} else if currentThemeDefault.get() != DefaultTheme.LIGHT.themeName {
ThemeManager.applyTheme(systemDarkThemeDefault.get())
}
.onChange(of: userInterfaceStyle) { _ in
sceneDelegate.window?.overrideUserInterfaceStyle = userInterfaceStyle
setUserInterfaceStyleDefault(userInterfaceStyle)
}
.onChange(of: uiTintColor) { _ in
sceneDelegate.window?.tintColor = UIColor(cgColor: uiTintColor)
@@ -166,24 +136,24 @@ func setUIAccentColorDefault(_ color: CGColor) {
}
}
//func getUserInterfaceStyleDefault() -> UIUserInterfaceStyle {
// switch UserDefaults.standard.integer(forKey: DEFAULT_USER_INTERFACE_STYLE) {
// case 1: return .light
// case 2: return .dark
// default: return .unspecified
// }
//}
//
//func setUserInterfaceStyleDefault(_ style: UIUserInterfaceStyle) {
// var v: Int
// switch style {
// case .unspecified: v = 0
// case .light: v = 1
// case .dark: v = 2
// default: v = 0
// }
// UserDefaults.standard.set(v, forKey: DEFAULT_USER_INTERFACE_STYLE)
//}
func getUserInterfaceStyleDefault() -> UIUserInterfaceStyle {
switch UserDefaults.standard.integer(forKey: DEFAULT_USER_INTERFACE_STYLE) {
case 1: return .light
case 2: return .dark
default: return .unspecified
}
}
func setUserInterfaceStyleDefault(_ style: UIUserInterfaceStyle) {
var v: Int
switch style {
case .unspecified: v = 0
case .light: v = 1
case .dark: v = 2
default: v = 0
}
UserDefaults.standard.set(v, forKey: DEFAULT_USER_INTERFACE_STYLE)
}
struct AppearanceSettings_Previews: PreviewProvider {
static var previews: some View {
@@ -22,7 +22,6 @@ struct CallSettings: View {
NavigationLink {
RTCServers()
.navigationTitle("Your ICE servers")
.modifier(ThemedBackground())
} label: {
Text("WebRTC ICE servers")
}
@@ -70,7 +70,6 @@ struct HiddenProfileView: View {
message: Text(savePasswordError ?? "")
)
}
.modifier(ThemedBackground())
}
var passwordValid: Bool { hidePassword == hidePassword.trimmingCharacters(in: .whitespaces) }
@@ -27,7 +27,6 @@ struct IncognitoHelp: View {
}
.listRowBackground(Color.clear)
}
.modifier(ThemedBackground())
}
}
@@ -30,7 +30,8 @@ private enum NetworkAlert: Identifiable {
struct NetworkAndServers: View {
@EnvironmentObject var m: ChatModel
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = true
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
@AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false
@State private var cfgLoaded = false
@State private var currentNetCfg = NetCfg.defaults
@State private var netCfg = NetCfg.defaults
@@ -47,7 +48,6 @@ struct NetworkAndServers: View {
NavigationLink {
ProtocolServersView(serverProtocol: .smp)
.navigationTitle("Your SMP servers")
.modifier(ThemedBackground())
} label: {
Text("SMP servers")
}
@@ -55,11 +55,12 @@ struct NetworkAndServers: View {
NavigationLink {
ProtocolServersView(serverProtocol: .xftp)
.navigationTitle("Your XFTP servers")
.modifier(ThemedBackground())
} label: {
Text("XFTP servers")
}
Toggle("Subscription percentage", isOn: $showSubscriptionPercentage)
Picker("Use .onion hosts", selection: $onionHosts) {
ForEach(OnionHosts.values, id: \.self) { Text($0.text) }
}
@@ -75,7 +76,6 @@ struct NetworkAndServers: View {
NavigationLink {
AdvancedNetworkSettings()
.navigationTitle("Network settings")
.modifier(ThemedBackground())
} label: {
Text("Advanced network settings")
}
@@ -113,7 +113,6 @@ struct NetworkAndServers: View {
NavigationLink {
RTCServers()
.navigationTitle("Your ICE servers")
.modifier(ThemedBackground())
} label: {
Text("WebRTC ICE servers")
}
@@ -33,7 +33,6 @@ struct NotificationsView: View {
}
}
.navigationTitle("Send notifications")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.inline)
.alert(item: $showAlert) { alert in
if let token = m.deviceToken {
@@ -69,7 +68,6 @@ struct NotificationsView: View {
}
}
.navigationTitle("Show preview")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.inline)
} label: {
HStack {
@@ -174,7 +172,6 @@ func ntfModeDescription(_ mode: NotificationsMode) -> LocalizedStringKey {
}
struct SelectionListView<Item: SelectableItem>: View {
@EnvironmentObject var theme: AppTheme
var list: [Item]
@Binding var selection: Item
var onSelection: ((Item) -> Void)?
@@ -188,11 +185,11 @@ struct SelectionListView<Item: SelectableItem>: View {
if selection == item {
Image(systemName: "checkmark")
.resizable().scaledToFit().frame(width: 16)
.foregroundColor(theme.colors.primary)
.foregroundColor(.accentColor)
}
}
.contentShape(Rectangle())
.listRowBackground(tapped == item ? Color(uiColor: .secondarySystemFill) : theme.colors.background)
.listRowBackground(Color(uiColor: tapped == item ? .secondarySystemFill : .systemBackground))
.onTapGesture {
if selection == item { return }
if let f = onSelection {
@@ -48,7 +48,6 @@ struct PrivacySettings: View {
NavigationLink {
SimplexLockView(prefPerformLA: $prefPerformLA, currentLAMode: $currentLAMode)
.navigationTitle("SimpleX Lock")
.modifier(ThemedBackground())
} label: {
if prefPerformLA {
settingsRow("lock.fill", color: .green) {
@@ -333,7 +332,6 @@ struct SimplexLockView: View {
@Binding var prefPerformLA: Bool
@Binding var currentLAMode: LAMode
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
@State private var laMode: LAMode = privacyLocalAuthModeDefault.get()
@AppStorage(DEFAULT_LA_LOCK_DELAY) private var laLockDelay = 30
@@ -418,7 +416,7 @@ struct SimplexLockView: View {
HStack(spacing: 6) {
Text("Enable self-destruct")
Image(systemName: "info.circle")
.foregroundColor(theme.colors.primary)
.foregroundColor(.accentColor)
.font(.system(size: 14))
}
.onTapGesture {
@@ -14,6 +14,7 @@ struct ProtocolServerView: View {
let serverProtocol: ServerProtocol
@Binding var server: ServerCfg
@State var serverToEdit: ServerCfg
@State var serverEnabled: Bool
@State private var showTestFailure = false
@State private var testing = false
@State private var testFailure: ProtocolTestFailure?
@@ -110,7 +111,10 @@ struct ProtocolServerView: View {
Spacer()
showTestStatus(server: serverToEdit)
}
Toggle("Use for new connections", isOn: $serverToEdit.enabled)
Toggle("Use for new connections", isOn: $serverEnabled)
.onChange(of: serverEnabled) { enabled in
serverToEdit.enabled = enabled ? .enabled : .disabled
}
}
}
}
@@ -179,7 +183,8 @@ struct ProtocolServerView_Previews: PreviewProvider {
ProtocolServerView(
serverProtocol: .smp,
server: Binding.constant(ServerCfg.sampleData.custom),
serverToEdit: ServerCfg.sampleData.custom
serverToEdit: ServerCfg.sampleData.custom,
serverEnabled: true
)
}
}
@@ -94,7 +94,6 @@ struct ProtocolServersView: View {
}
.sheet(isPresented: $showScanProtoServer) {
ScanProtocolServer(servers: $servers)
.modifier(ThemedBackground())
}
.modifier(BackButton(disabled: Binding.constant(false)) {
if saveDisabled {
@@ -160,7 +159,7 @@ struct ProtocolServersView: View {
}
private var allServersDisabled: Bool {
servers.allSatisfy { !$0.enabled }
servers.allSatisfy { $0.enabled != .enabled }
}
private func protocolServerView(_ server: Binding<ServerCfg>) -> some View {
@@ -169,10 +168,10 @@ struct ProtocolServersView: View {
ProtocolServerView(
serverProtocol: serverProtocol,
server: server,
serverToEdit: srv
serverToEdit: srv,
serverEnabled: srv.enabled == .enabled
)
.navigationBarTitle(srv.preset ? "Preset server" : "Your server")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
let address = parseServerAddress(srv.server)
@@ -183,7 +182,7 @@ struct ProtocolServersView: View {
invalidServer()
} else if !uniqueAddress(srv, address) {
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
} else if !srv.enabled {
} else if srv.enabled != .enabled {
Image(systemName: "slash.circle").foregroundColor(.secondary)
} else {
showTestStatus(server: srv)
@@ -196,7 +195,7 @@ struct ProtocolServersView: View {
.padding(.trailing, 4)
let v = Text(address?.hostnames.first ?? srv.server).lineLimit(1)
if srv.enabled {
if srv.enabled == .enabled {
v
} else {
v.foregroundColor(.secondary)
@@ -237,7 +236,7 @@ struct ProtocolServersView: View {
private func addAllPresets() {
for srv in presetServers {
if !hasPreset(srv) {
servers.append(ServerCfg(server: srv, preset: true, tested: nil, enabled: true))
servers.append(ServerCfg(server: srv, preset: true, tested: nil, enabled: .enabled))
}
}
}
@@ -262,7 +261,7 @@ struct ProtocolServersView: View {
private func resetTestStatus() {
for i in 0..<servers.count {
if servers[i].enabled {
if servers[i].enabled == .enabled {
servers[i].tested = nil
}
}
@@ -271,7 +270,7 @@ struct ProtocolServersView: View {
private func runServersTest() async -> [String: ProtocolTestFailure] {
var fs: [String: ProtocolTestFailure] = [:]
for i in 0..<servers.count {
if servers[i].enabled {
if servers[i].enabled == .enabled {
if let f = await testServerConnection(server: $servers[i]) {
fs[serverHostname(servers[i].server)] = f
}
@@ -40,7 +40,7 @@ struct ScanProtocolServer: View {
switch resp {
case let .success(r):
if parseServerAddress(r.string) != nil {
servers.append(ServerCfg(server: r.string, preset: false, tested: nil, enabled: true))
servers.append(ServerCfg(server: r.string, preset: false, tested: nil, enabled: .enabled))
dismiss()
} else {
showAddressError = true

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