mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
153 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed16f71ee4 | |||
| 5bc70bdfed | |||
| fb4927ca87 | |||
| 2023464a13 | |||
| b8bf5871fb | |||
| 1573b7af13 | |||
| a5bf3cbca8 | |||
| d5529031ee | |||
| d244338b13 | |||
| ce55c84b96 | |||
| c52f9baefa | |||
| ea1c5da65d | |||
| 5695bb9d46 | |||
| 13751d5705 | |||
| 02803b1491 | |||
| 222af69a71 | |||
| 5e96e1ea2b | |||
| 490e8cead8 | |||
| f578ee843b | |||
| cf2eb76cb7 | |||
| 48f9dcab9a | |||
| ac2ef54838 | |||
| 7e269c6b8c | |||
| 3e739924bb | |||
| 83706b19da | |||
| 6f19a7a20f | |||
| a8740cee4b | |||
| 4000fe383d | |||
| 75445e604e | |||
| 0596278e50 | |||
| 7a77775a42 | |||
| dc8d08ec04 | |||
| 4ad0e0f65e | |||
| d01515e755 | |||
| 3fa70ba217 | |||
| 6ae4cddc2e | |||
| ac8ee971f5 | |||
| 11d4872186 | |||
| 370e2e87ad | |||
| ef30fcf5e2 | |||
| 6ad1423f29 | |||
| c52e5349db | |||
| 3c98c6acc0 | |||
| c251f5e2bd | |||
| b165603136 | |||
| 184a24b70e | |||
| d46bae6498 | |||
| 5ac521f6d8 | |||
| 7b5b747b19 | |||
| 037655e30f | |||
| 54088c6d5a | |||
| 78f7fcc89f | |||
| b402caeb89 | |||
| ff8a8de3c8 | |||
| d22c0c2614 | |||
| 73c8d3c2e3 | |||
| 0110385e65 | |||
| ad0a678c5f | |||
| 0495a4cd78 | |||
| 02267c5263 | |||
| cad3bc048f | |||
| 1b04423745 | |||
| 2143eb2d7a | |||
| 3d395b0e45 | |||
| d79b4dfb5a | |||
| 0ae9f5ebba | |||
| 57c80786dc | |||
| da1cad96fc | |||
| b58ccedf23 | |||
| 01cadefde7 | |||
| c70e7223d9 | |||
| a1d4f52185 | |||
| 373070f388 | |||
| a03ccd0ba6 | |||
| a7982cccf9 | |||
| dccd6631af | |||
| b0516b6440 | |||
| bf91666f07 | |||
| ab47a5a27e | |||
| 23581f59ae | |||
| 291df6e9d0 | |||
| dae0b63c22 | |||
| 0964982782 | |||
| 9eb6034e1a | |||
| c59eb06b8a | |||
| 423fc96638 | |||
| ba203faad4 | |||
| ec7b35adb9 | |||
| d2d450d1d7 | |||
| b4caefb17c | |||
| fd791f3fb8 | |||
| 0720292ebf | |||
| a0d6ae15ab | |||
| 3e76a2d49c | |||
| ed3365bb6d | |||
| b2a8797ed4 | |||
| d3e522e169 | |||
| 49e09ca449 | |||
| d1dde3d0c8 | |||
| ef75eceb40 | |||
| 96ce59f330 | |||
| 98a67fa2e4 | |||
| 4b13512950 | |||
| 4c0d47bbd4 | |||
| 3129602a78 | |||
| a0c257962e | |||
| 93ae1145bc | |||
| d02668386f | |||
| bd8c8b5a8c | |||
| e359ec02f5 | |||
| a1948d549b | |||
| 396b73b480 | |||
| 331b4633fc | |||
| 29db2b4e9b | |||
| f091384aa7 | |||
| f40ba6f04d | |||
| 06d61ea73e | |||
| 84d7a77a9f | |||
| 878eea774d | |||
| 4575dc6e30 | |||
| 554ef4eefc | |||
| 567e9981be | |||
| 1183fe427f | |||
| c49a1f42e7 | |||
| 4f173215c6 | |||
| 41597e563e | |||
| aa127f0cea | |||
| f9c64ba39c | |||
| 1025fa94a1 | |||
| a59769b91d | |||
| 76248b4878 | |||
| 93a7ddb128 | |||
| bc5af35a3e | |||
| e38d5bd885 | |||
| 10f41aa1c9 | |||
| 1b081197d4 | |||
| 9615e525c0 | |||
| db136116d3 | |||
| 98f5e5d22b | |||
| 9f33e5d35e | |||
| b8e8124faf | |||
| 156e41950a | |||
| 26c5ed4caf | |||
| d832afa7e8 | |||
| 2e18b97077 | |||
| b6e57c0fa2 | |||
| 15a226cfd1 | |||
| c4cbf751b9 | |||
| 535ff1d836 | |||
| fb0718adac | |||
| 5f501f1cb9 | |||
| 7cc86574fe | |||
| 19fc44efae |
@@ -65,6 +65,11 @@ jobs:
|
||||
asset_name: simplex-chat-ubuntu-22_04-x86-64
|
||||
desktop_asset_name: simplex-desktop-ubuntu-22_04-x86_64.deb
|
||||
- os: macos-latest
|
||||
ghc: "9.6.3"
|
||||
cache_path: ~/.cabal/store
|
||||
asset_name: simplex-chat-macos-aarch64
|
||||
desktop_asset_name: simplex-desktop-macos-aarch64.dmg
|
||||
- os: macos-13
|
||||
ghc: "9.6.3"
|
||||
cache_path: ~/.cabal/store
|
||||
asset_name: simplex-chat-macos-x86-64
|
||||
@@ -107,18 +112,36 @@ jobs:
|
||||
if: matrix.os == 'macos-latest'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "ignore-project: False" >> cabal.project.local
|
||||
echo "package direct-sqlcipher" >> cabal.project.local
|
||||
echo " extra-include-dirs: /usr/local/opt/openssl@1.1/include" >> cabal.project.local
|
||||
echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local
|
||||
echo " flags: +openssl" >> cabal.project.local
|
||||
echo "ignore-project: False" >> cabal.project.local
|
||||
echo "package simplexmq" >> cabal.project.local
|
||||
echo " extra-include-dirs: /opt/homebrew/opt/openssl@1.1/include" >> cabal.project.local
|
||||
echo " extra-lib-dirs: /opt/homebrew/opt/openssl@1.1/lib" >> cabal.project.local
|
||||
echo "" >> cabal.project.local
|
||||
echo "package direct-sqlcipher" >> cabal.project.local
|
||||
echo " extra-include-dirs: /opt/homebrew/opt/openssl@1.1/include" >> cabal.project.local
|
||||
echo " extra-lib-dirs: /opt/homebrew/opt/openssl@1.1/lib" >> cabal.project.local
|
||||
echo " flags: +openssl" >> cabal.project.local
|
||||
|
||||
- name: Unix prepare cabal.project.local for Mac
|
||||
if: matrix.os == 'macos-13'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "ignore-project: False" >> cabal.project.local
|
||||
echo "package simplexmq" >> cabal.project.local
|
||||
echo " extra-include-dirs: /usr/local/opt/openssl@1.1/include" >> cabal.project.local
|
||||
echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local
|
||||
echo "" >> cabal.project.local
|
||||
echo "package direct-sqlcipher" >> cabal.project.local
|
||||
echo " extra-include-dirs: /usr/local/opt/openssl@1.1/include" >> cabal.project.local
|
||||
echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local
|
||||
echo " flags: +openssl" >> cabal.project.local
|
||||
|
||||
- name: Install AppImage dependencies
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
|
||||
run: sudo apt install -y desktop-file-utils
|
||||
|
||||
- name: Install pkg-config for Mac
|
||||
if: matrix.os == 'macos-latest'
|
||||
if: matrix.os == 'macos-latest' || matrix.os == 'macos-13'
|
||||
run: brew install pkg-config
|
||||
|
||||
- name: Unix prepare cabal.project.local for Ubuntu
|
||||
@@ -190,7 +213,7 @@ jobs:
|
||||
|
||||
- name: Mac build desktop
|
||||
id: mac_desktop_build
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest'
|
||||
if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'macos-latest' || matrix.os == 'macos-13')
|
||||
shell: bash
|
||||
env:
|
||||
APPLE_SIMPLEX_SIGNING_KEYCHAIN: ${{ secrets.APPLE_SIMPLEX_SIGNING_KEYCHAIN }}
|
||||
@@ -241,7 +264,7 @@ jobs:
|
||||
${{ steps.linux_appimage_build.outputs.appimage_hash }}
|
||||
|
||||
- name: Mac upload desktop package to release
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest'
|
||||
if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'macos-latest' || matrix.os == 'macos-13')
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -250,7 +273,7 @@ jobs:
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
- name: Mac update desktop package hash
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest'
|
||||
if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'macos-latest' || matrix.os == 'macos-13')
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
+4
-3
@@ -1,5 +1,6 @@
|
||||
---
|
||||
layout: layouts/privacy.html
|
||||
permalink: /privacy/index.html
|
||||
---
|
||||
|
||||
# SimpleX Chat Privacy Policy and Conditions of Use
|
||||
@@ -10,7 +11,7 @@ SimpleX Chat communication protocol is the first protocol that has no user profi
|
||||
|
||||
Double ratchet algorithm has such important properties as [forward secrecy](/docs/GLOSSARY.md#forward-secrecy), sender [repudiation](/docs/GLOSSARY.md#) and break-in recovery (also known as [post-compromise security](/docs/GLOSSARY.md#post-compromise-security)).
|
||||
|
||||
If you believe that any part of this document is not aligned with our mission or values, please raise it with us via [email](chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
|
||||
If you believe that any part of this document is not aligned with our mission or values, please raise it with us via [email](mailto:chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
|
||||
|
||||
## Privacy Policy
|
||||
|
||||
@@ -78,7 +79,7 @@ When you choose to use instant push notifications in SimpleX iOS app, because th
|
||||
|
||||
Preset notification server cannot observe the actual addresses of these queues, as a separate address is used to subscribe to the notifications. It also cannot observe who sends messages to you. Apple push notifications servers can only observe how many notifications are sent to you, but not from how many contacts, or from which messaging relays, as notifications are delivered to your device end-to-end encrypted by one of the preset notification servers - these notifications only contain end-to-end encrypted metadata, not even encrypted message content, and they look completely random to Apple push notification servers.
|
||||
|
||||
You can read more about the design of iOS push notifications [here](https://simplex.chat/blog/20220404-simplex-chat-instant-notifications.html#our-ios-approach-has-one-trade-off).
|
||||
You can read more about the design of iOS push notifications [here](./blog/20220404-simplex-chat-instant-notifications.md#our-ios-approach-has-one-trade-off).
|
||||
|
||||
#### Another information stored on the servers
|
||||
|
||||
@@ -115,7 +116,7 @@ We will update this Privacy Policy as needed so that it is current, accurate, an
|
||||
|
||||
Please also read our Conditions of Use of Software and Infrastructure below.
|
||||
|
||||
If you have questions about our Privacy Policy please contact us via [email](chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
|
||||
If you have questions about our Privacy Policy please contact us via [email](mailto:chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
|
||||
|
||||
## Conditions of Use of Software and Infrastructure
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ Messages not following these rules will be deleted, the right to send messages m
|
||||
|
||||
You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
|
||||
|
||||
There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F6eHqy7uAbZPOcA6qBtrQgQquVlt4Ll91%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAqV_pg3FF00L98aCXp4D3bOs4Sxv_UmSd-gb0juVoQVs%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22XonlixcHBIb2ijCehbZoiw%3D%3D%22%7D) for developers who build on SimpleX platform:
|
||||
There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) for developers who build on SimpleX platform:
|
||||
|
||||
- chat bots and automations
|
||||
- integrations with other apps
|
||||
@@ -234,6 +234,8 @@ You can use SimpleX with your own servers and still communicate with people usin
|
||||
|
||||
Recent and important updates:
|
||||
|
||||
[Jun 4, 2024. SimpleX network: private message routing, v5.8 released with IP address protection and chat themes](./blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md)
|
||||
|
||||
[Apr 26, 2024. SimpleX network: legally binding transparency, v5.7 released with better calls and messages.](./blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md)
|
||||
|
||||
[Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](./blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md)
|
||||
@@ -382,10 +384,10 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
|
||||
- ✅ Private notes.
|
||||
- ✅ Improve sending videos (including encryption of locally stored videos).
|
||||
- ✅ Post-quantum resistant key exchange in double ratchet protocol.
|
||||
- ✅ Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
|
||||
- 🏗 Improve stability and reduce battery usage.
|
||||
- 🏗 Improve experience for the new users.
|
||||
- 🏗 Large groups, communities and public channels.
|
||||
- 🏗 Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
|
||||
- Privacy & security slider - a simple way to set all settings at once.
|
||||
- SMP queue redundancy and rotation (manual is supported).
|
||||
- Include optional message into connection request sent via contact address.
|
||||
|
||||
@@ -17,6 +17,7 @@ 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
|
||||
}
|
||||
|
||||
@@ -141,6 +142,10 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private func prepareForLaunch() {
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
static func keepScreenOn(_ on: Bool) {
|
||||
UIApplication.shared.isIdleTimerDisabled = on
|
||||
}
|
||||
@@ -148,13 +153,15 @@ 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
|
||||
window?.tintColor = UIColor(cgColor: getUIAccentColorDefault())
|
||||
window?.overrideUserInterfaceStyle = getUserInterfaceStyleDefault()
|
||||
SceneDelegate.windowStatic = windowScene.keyWindow
|
||||
ThemeManager.applyTheme(currentThemeDefault.get())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_cats@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_flowers@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_hearts@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_kids@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_school@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "wallpaper_travel@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
@@ -14,6 +14,8 @@ 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
|
||||
|
||||
@@ -51,6 +53,16 @@ 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.
|
||||
@@ -96,6 +108,8 @@ struct ContentView: View {
|
||||
initializationView()
|
||||
}
|
||||
}
|
||||
//.tint(theme.colors.primary)
|
||||
.background(theme.colors.background)
|
||||
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
|
||||
.sheet(isPresented: $showSettings) {
|
||||
SettingsView(showSettings: $showSettings)
|
||||
@@ -138,6 +152,11 @@ struct ContentView: View {
|
||||
break
|
||||
}
|
||||
}
|
||||
.onChange(of: colorScheme) { scheme in
|
||||
if sceneDelegate.window?.overrideUserInterfaceStyle == .unspecified {
|
||||
reactOnDarkThemeChanges(scheme == .dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func contentView() -> some View {
|
||||
@@ -224,8 +243,8 @@ struct ContentView: View {
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity )
|
||||
.background(
|
||||
Rectangle()
|
||||
.fill(.background)
|
||||
)
|
||||
.fill(theme.colors.background)
|
||||
)
|
||||
}
|
||||
|
||||
private func mainView() -> some View {
|
||||
|
||||
@@ -422,6 +422,16 @@ 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 {
|
||||
|
||||
@@ -205,6 +205,43 @@ 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)
|
||||
}
|
||||
|
||||
@@ -242,14 +242,8 @@ func apiSuspendChat(timeoutMicroseconds: Int) {
|
||||
logger.error("apiSuspendChat error: \(String(describing: r))")
|
||||
}
|
||||
|
||||
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)
|
||||
func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String, ctrl: chat_ctrl? = nil) throws {
|
||||
let r = chatSendCmdSync(.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder), ctrl)
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
@@ -341,8 +335,8 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64) async -> ChatItem? {
|
||||
let cmd: ChatCommand = .apiForwardChatItem(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemId: itemId)
|
||||
func apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64, ttl: Int?) async -> ChatItem? {
|
||||
let cmd: ChatCommand = .apiForwardChatItem(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemId: itemId, ttl: ttl)
|
||||
return await processSendMessageCmd(toChatType: toChatType, cmd: cmd)
|
||||
}
|
||||
|
||||
@@ -561,6 +555,18 @@ func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (Gro
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) {
|
||||
let r = await chatSendCmd(.apiContactQueueInfo(contactId: contactId))
|
||||
if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) {
|
||||
let r = await chatSendCmd(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId))
|
||||
if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSwitchContact(contactId: Int64) throws -> ConnectionStats {
|
||||
let r = chatSendCmdSync(.apiSwitchContact(contactId: contactId))
|
||||
if case let .contactSwitchStarted(_, _, connectionStats) = r { return connectionStats }
|
||||
@@ -819,6 +825,21 @@ 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))
|
||||
@@ -927,14 +948,19 @@ func standaloneFileInfo(url: String, ctrl: chat_ctrl? = nil) async -> MigrationF
|
||||
}
|
||||
}
|
||||
|
||||
func receiveFile(user: any UserLike, fileId: Int64, auto: Bool = false) async {
|
||||
if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get(), auto: auto) {
|
||||
func receiveFile(user: any UserLike, fileId: Int64, userApprovedRelays: Bool = false, auto: Bool = false) async {
|
||||
if let chatItem = await apiReceiveFile(
|
||||
fileId: fileId,
|
||||
userApprovedRelays: userApprovedRelays || !privacyAskToApproveRelaysGroupDefault.get(),
|
||||
encrypted: privacyEncryptLocalFilesGroupDefault.get(),
|
||||
auto: auto
|
||||
) {
|
||||
await chatItemSimpleUpdate(user, chatItem)
|
||||
}
|
||||
}
|
||||
|
||||
func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? {
|
||||
let r = await chatSendCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline))
|
||||
func apiReceiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? {
|
||||
let r = await chatSendCmd(.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline))
|
||||
let am = AlertManager.shared
|
||||
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
|
||||
if case .rcvFileAcceptedSndCancelled = r {
|
||||
@@ -947,19 +973,50 @@ func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil, auto: B
|
||||
}
|
||||
} else if let networkErrorAlert = networkErrorAlert(r) {
|
||||
logger.error("apiReceiveFile network error: \(String(describing: r))")
|
||||
am.showAlert(networkErrorAlert)
|
||||
if !auto {
|
||||
am.showAlert(networkErrorAlert)
|
||||
}
|
||||
} else {
|
||||
switch chatError(r) {
|
||||
case .fileCancelled:
|
||||
logger.debug("apiReceiveFile ignoring fileCancelled error")
|
||||
case .fileAlreadyReceiving:
|
||||
logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error")
|
||||
case let .fileNotApproved(fileId, unknownServers):
|
||||
logger.debug("apiReceiveFile fileNotApproved error")
|
||||
if !auto {
|
||||
let srvs = unknownServers.map { s in
|
||||
if let srv = parseServerAddress(s), !srv.hostnames.isEmpty {
|
||||
srv.hostnames[0]
|
||||
} else {
|
||||
serverHost(s)
|
||||
}
|
||||
}
|
||||
am.showAlert(Alert(
|
||||
title: Text("Unknown servers!"),
|
||||
message: Text("Without Tor or VPN, your IP address will be visible to these XFTP relays: \(srvs.sorted().joined(separator: ", "))."),
|
||||
primaryButton: .default(
|
||||
Text("Download"),
|
||||
action: {
|
||||
Task {
|
||||
logger.debug("apiReceiveFile fileNotApproved alert - in Task")
|
||||
if let user = ChatModel.shared.currentUser {
|
||||
await receiveFile(user: user, fileId: fileId, userApprovedRelays: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
secondaryButton: .cancel()
|
||||
))
|
||||
}
|
||||
default:
|
||||
logger.error("apiReceiveFile error: \(String(describing: r))")
|
||||
am.showAlertMsg(
|
||||
title: "Error receiving file",
|
||||
message: "Error: \(String(describing: r))"
|
||||
)
|
||||
if !auto {
|
||||
am.showAlertMsg(
|
||||
title: "Error receiving file",
|
||||
message: "Error: \(String(describing: r))"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -1229,7 +1286,7 @@ func filterMembersToAdd(_ ms: [GMember]) -> [Contact] {
|
||||
let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil }
|
||||
return ChatModel.shared.chats
|
||||
.compactMap{ $0.chatInfo.contact }
|
||||
.filter{ c in c.ready && c.active && !memberContactIds.contains(c.apiId) }
|
||||
.filter{ c in c.sendMsgEnabled && !c.nextSendGrpInv && !memberContactIds.contains(c.apiId) }
|
||||
.sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() }
|
||||
}
|
||||
|
||||
@@ -1305,8 +1362,7 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni
|
||||
if encryptionStartedDefault.get() {
|
||||
encryptionStartedDefault.set(false)
|
||||
}
|
||||
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
|
||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
try apiSetAppFilePaths(filesFolder: getAppFilesDirectory().path, tempFolder: getTempFilesDirectory().path, assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path)
|
||||
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
||||
m.chatInitialized = true
|
||||
m.currentUser = try apiGetActiveUser()
|
||||
@@ -1391,8 +1447,7 @@ 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 apiSetTempFolder(tempFolder: getMigrationTempFilesDirectory().path, ctrl: ctrl)
|
||||
try apiSetFilesFolder(filesFolder: getMigrationTempFilesDirectory().path, ctrl: ctrl)
|
||||
try apiSetAppFilePaths(filesFolder: getMigrationTempFilesDirectory().path, tempFolder: getMigrationTempFilesDirectory().path, assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path, ctrl: ctrl)
|
||||
_ = try apiStartChat(ctrl: ctrl)
|
||||
return migrationActiveUser
|
||||
}
|
||||
@@ -1778,11 +1833,15 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
if let aChatItem = aChatItem {
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
}
|
||||
case let .rcvFileError(user, aChatItem, _):
|
||||
case let .rcvFileError(user, aChatItem, _, _):
|
||||
if let aChatItem = aChatItem {
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupFile(aChatItem) }
|
||||
}
|
||||
case let .rcvFileWarning(user, aChatItem, _, _):
|
||||
if let aChatItem = aChatItem {
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
}
|
||||
case let .sndFileStart(user, aChatItem, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .sndFileComplete(user, aChatItem, _):
|
||||
@@ -1799,11 +1858,15 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
}
|
||||
case let .sndFileCompleteXFTP(user, aChatItem, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .sndFileError(user, aChatItem, _):
|
||||
case let .sndFileError(user, aChatItem, _, _):
|
||||
if let aChatItem = aChatItem {
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupFile(aChatItem) }
|
||||
}
|
||||
case let .sndFileWarning(user, aChatItem, _, _):
|
||||
if let aChatItem = aChatItem {
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
}
|
||||
case let .callInvitation(invitation):
|
||||
await MainActor.run {
|
||||
m.callInvitations[invitation.contact.id] = invitation
|
||||
@@ -1849,21 +1912,35 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
}
|
||||
case .chatSuspended:
|
||||
chatSuspended()
|
||||
case let .contactSwitch(_, contact, switchProgress):
|
||||
await MainActor.run {
|
||||
m.updateContactConnectionStats(contact, switchProgress.connectionStats)
|
||||
case let .contactSwitch(user, contact, switchProgress):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateContactConnectionStats(contact, switchProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
case let .groupMemberSwitch(_, groupInfo, member, switchProgress):
|
||||
await MainActor.run {
|
||||
m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats)
|
||||
case let .groupMemberSwitch(user, groupInfo, member, switchProgress):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
case let .contactRatchetSync(_, contact, ratchetSyncProgress):
|
||||
await MainActor.run {
|
||||
m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats)
|
||||
case let .contactRatchetSync(user, contact, ratchetSyncProgress):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
case let .groupMemberRatchetSync(_, groupInfo, member, ratchetSyncProgress):
|
||||
await MainActor.run {
|
||||
m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats)
|
||||
case let .groupMemberRatchetSync(user, groupInfo, member, ratchetSyncProgress):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
case let .contactDisabled(user, contact):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateContact(contact)
|
||||
}
|
||||
}
|
||||
case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible):
|
||||
await MainActor.run {
|
||||
@@ -1887,12 +1964,28 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
let state = UIRemoteCtrlSessionState.connected(remoteCtrl: remoteCtrl, sessionCode: m.remoteCtrlSession?.sessionCode ?? "")
|
||||
m.remoteCtrlSession = m.remoteCtrlSession?.updateState(state)
|
||||
}
|
||||
case .remoteCtrlStopped:
|
||||
case let .remoteCtrlStopped(_, rcStopReason):
|
||||
// This delay is needed to cancel the session that fails on network failure,
|
||||
// e.g. when user did not grant permission to access local network yet.
|
||||
if let sess = m.remoteCtrlSession {
|
||||
await MainActor.run {
|
||||
m.remoteCtrlSession = nil
|
||||
dismissAllSheets() {
|
||||
switch rcStopReason {
|
||||
case .connectionFailed(.errorAgent(.RCP(.identity))):
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Connection with desktop stopped",
|
||||
message: "This link was used with another mobile device, please create a new link on the desktop."
|
||||
)
|
||||
default:
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Connection with desktop stopped"),
|
||||
message: Text("Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers."),
|
||||
primaryButton: .default(Text("Ok")),
|
||||
secondaryButton: .default(Text("Copy error")) { UIPasteboard.general.string = String(describing: rcStopReason) }
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
if case .connected = sess.sessionState {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
|
||||
@@ -39,6 +39,7 @@ 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
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//
|
||||
// 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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
//
|
||||
// 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 Stanislav on 24.04.2024.
|
||||
// Created by Avently on 24.04.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct IncomingCallView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var cc = CallController.shared
|
||||
|
||||
var body: some View {
|
||||
@@ -43,7 +44,7 @@ struct IncomingCallView: View {
|
||||
cc.endCall(invitation: invitation)
|
||||
}
|
||||
|
||||
callButton("Ignore", "multiply", .accentColor) {
|
||||
callButton("Ignore", "multiply", theme.colors.primary) {
|
||||
cc.activeCallInvitation = nil
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ struct IncomingCallView: View {
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.modifier(ThemedBackground())
|
||||
.onAppear { dismissAllSheets() }
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ func localizedInfoRow(_ title: LocalizedStringKey, _ value: LocalizedStringKey)
|
||||
}
|
||||
}
|
||||
|
||||
private func serverHost(_ s: String) -> String {
|
||||
func serverHost(_ s: String) -> String {
|
||||
if let i = s.range(of: "@")?.lowerBound {
|
||||
return String(s[i...].dropFirst())
|
||||
} else {
|
||||
@@ -90,6 +90,7 @@ 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
|
||||
@@ -110,6 +111,7 @@ struct ChatInfoView: View {
|
||||
case switchAddressAlert
|
||||
case abortSwitchAddressAlert
|
||||
case syncConnectionForceAlert
|
||||
case queueInfo(info: String)
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
||||
|
||||
var id: String {
|
||||
@@ -119,6 +121,7 @@ struct ChatInfoView: View {
|
||||
case .switchAddressAlert: return "switchAddressAlert"
|
||||
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
|
||||
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
|
||||
case let .queueInfo(info): return "queueInfo \(info)"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
@@ -224,9 +227,22 @@ struct ChatInfoView: View {
|
||||
Section(header: Text("For console")) {
|
||||
infoRow("Local name", chat.chatInfo.localDisplayName)
|
||||
infoRow("Database ID", "\(chat.chatInfo.apiId)")
|
||||
Button ("Debug delivery") {
|
||||
Task {
|
||||
do {
|
||||
let info = queueInfoText(try await apiContactQueueInfo(chat.chatInfo.apiId))
|
||||
await MainActor.run { alert = .queueInfo(info: info) }
|
||||
} catch let e {
|
||||
logger.error("apiContactQueueInfo error: \(responseError(e))")
|
||||
let a = getErrorAlert(e, "Error")
|
||||
await MainActor.run { alert = .error(title: a.title, error: a.message) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
@@ -243,6 +259,7 @@ struct ChatInfoView: View {
|
||||
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
|
||||
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress)
|
||||
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) })
|
||||
case let .queueInfo(info): return queueInfoAlert(info)
|
||||
case let .error(title, error): return mkAlert(title: title, message: error)
|
||||
}
|
||||
}
|
||||
@@ -355,6 +372,7 @@ struct ChatInfoView: View {
|
||||
)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("Security code")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Label(
|
||||
contact.verified ? "View security code" : "Verify security code",
|
||||
@@ -371,6 +389,7 @@ struct ChatInfoView: View {
|
||||
currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences)
|
||||
)
|
||||
.navigationBarTitle("Contact preferences")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
Label("Contact preferences", systemImage: "switch.2")
|
||||
@@ -419,7 +438,7 @@ struct ChatInfoView: View {
|
||||
HStack {
|
||||
Text("Network status")
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.font(.system(size: 14))
|
||||
Spacer()
|
||||
Text(chatModel.contactNetworkStatus(contact).statusString)
|
||||
@@ -577,6 +596,22 @@ func syncConnectionForceAlert(_ syncConnectionForce: @escaping () -> Void) -> Al
|
||||
)
|
||||
}
|
||||
|
||||
func queueInfoText(_ info: (RcvMsgInfo?, QueueInfo)) -> String {
|
||||
let (rcvMsgInfo, qInfo) = info
|
||||
var msgInfo: String
|
||||
if let rcvMsgInfo { msgInfo = encodeJSON(rcvMsgInfo) } else { msgInfo = "none" }
|
||||
return String.localizedStringWithFormat(NSLocalizedString("server queue info: %@\n\nlast received msg: %@", comment: "queue info"), encodeJSON(qInfo), msgInfo)
|
||||
}
|
||||
|
||||
func queueInfoAlert(_ info: String) -> Alert {
|
||||
Alert(
|
||||
title: Text("Message queue info"),
|
||||
message: Text(info),
|
||||
primaryButton: .default(Text("Ok")),
|
||||
secondaryButton: .default(Text("Copy")) { UIPasteboard.general.string = info }
|
||||
)
|
||||
}
|
||||
|
||||
struct ChatInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ChatInfoView(
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CICallItemView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var status: CICallStatus
|
||||
@@ -35,7 +36,7 @@ struct CICallItemView: View {
|
||||
case .error: missedCallIcon(sent).foregroundColor(.orange)
|
||||
}
|
||||
|
||||
CIMetaView(chat: chat, chatItem: chatItem, showStatus: false, showEdited: false)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary, showStatus: false, showEdited: false)
|
||||
.padding(.bottom, 8)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CIFeaturePreferenceView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
var feature: ChatFeature
|
||||
var allowed: FeatureAllowed
|
||||
@@ -45,7 +46,7 @@ struct CIFeaturePreferenceView: View {
|
||||
r = r
|
||||
+ Text(acceptText)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
+ Text(" ")
|
||||
}
|
||||
r = r + chatItem.timestampText
|
||||
|
||||
@@ -12,6 +12,7 @@ import SimpleXChat
|
||||
struct CIFileView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
let file: CIFile?
|
||||
let edited: Bool
|
||||
|
||||
@@ -54,15 +55,18 @@ struct CIFileView: View {
|
||||
switch (file.fileStatus) {
|
||||
case .sndStored: return file.fileProtocol == .local
|
||||
case .sndTransfer: return false
|
||||
case .sndComplete: return false
|
||||
case .sndComplete: return true
|
||||
case .sndCancelled: return false
|
||||
case .sndError: return false
|
||||
case .sndError: return true
|
||||
case .sndWarning: return true
|
||||
case .rcvInvitation: return true
|
||||
case .rcvAccepted: return true
|
||||
case .rcvTransfer: return false
|
||||
case .rcvAborted: return true
|
||||
case .rcvComplete: return true
|
||||
case .rcvCancelled: return false
|
||||
case .rcvError: return false
|
||||
case .rcvError: return true
|
||||
case .rcvWarning: return true
|
||||
case .invalid: return false
|
||||
}
|
||||
}
|
||||
@@ -73,10 +77,10 @@ struct CIFileView: View {
|
||||
logger.debug("CIFileView fileAction")
|
||||
if let file = file {
|
||||
switch (file.fileStatus) {
|
||||
case .rcvInvitation:
|
||||
case .rcvInvitation, .rcvAborted:
|
||||
if fileSizeValid(file) {
|
||||
Task {
|
||||
logger.debug("CIFileView fileAction - in .rcvInvitation, in Task")
|
||||
logger.debug("CIFileView fileAction - in .rcvInvitation, .rcvAborted, in Task")
|
||||
if let user = m.currentUser {
|
||||
await receiveFile(user: user, fileId: file.fileId)
|
||||
}
|
||||
@@ -107,11 +111,40 @@ struct CIFileView: View {
|
||||
if let fileSource = getLoadedFileSource(file) {
|
||||
saveCryptoFile(fileSource)
|
||||
}
|
||||
case let .rcvError(rcvFileError):
|
||||
logger.debug("CIFileView fileAction - in .rcvError")
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
case let .rcvWarning(rcvFileError):
|
||||
logger.debug("CIFileView fileAction - in .rcvWarning")
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
case .sndStored:
|
||||
logger.debug("CIFileView fileAction - in .sndStored")
|
||||
if file.fileProtocol == .local, let fileSource = getLoadedFileSource(file) {
|
||||
saveCryptoFile(fileSource)
|
||||
}
|
||||
case .sndComplete:
|
||||
logger.debug("CIFileView fileAction - in .sndComplete")
|
||||
if let fileSource = getLoadedFileSource(file) {
|
||||
saveCryptoFile(fileSource)
|
||||
}
|
||||
case let .sndError(sndFileError):
|
||||
logger.debug("CIFileView fileAction - in .sndError")
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
case let .sndWarning(sndFileError):
|
||||
logger.debug("CIFileView fileAction - in .sndWarning")
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
default: break
|
||||
}
|
||||
}
|
||||
@@ -135,9 +168,10 @@ struct CIFileView: View {
|
||||
case .sndComplete: fileIcon("doc.fill", innerIcon: "checkmark", innerIconSize: 10)
|
||||
case .sndCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
case .sndError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
case .sndWarning: fileIcon("doc.fill", innerIcon: "exclamationmark.triangle.fill", innerIconSize: 10)
|
||||
case .rcvInvitation:
|
||||
if fileSizeValid(file) {
|
||||
fileIcon("arrow.down.doc.fill", color: .accentColor)
|
||||
fileIcon("arrow.down.doc.fill", color: theme.colors.primary)
|
||||
} else {
|
||||
fileIcon("doc.fill", color: .orange, innerIcon: "exclamationmark", innerIconSize: 12)
|
||||
}
|
||||
@@ -148,9 +182,12 @@ struct CIFileView: View {
|
||||
} else {
|
||||
progressView()
|
||||
}
|
||||
case .rcvAborted:
|
||||
fileIcon("doc.fill", color: theme.colors.primary, 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)
|
||||
case .rcvWarning: fileIcon("doc.fill", innerIcon: "exclamationmark.triangle.fill", innerIconSize: 10)
|
||||
case .invalid: fileIcon("doc.fill", innerIcon: "questionmark", innerIconSize: 10)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -11,7 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CIGroupInvitationView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var groupInvitation: CIGroupInvitation
|
||||
@@ -21,6 +21,8 @@ struct CIGroupInvitationView: View {
|
||||
@State private var inProgress = false
|
||||
@State private var progressByTimeout = false
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
|
||||
|
||||
var body: some View {
|
||||
let action = !chatItem.chatDir.sent && groupInvitation.status == .pending
|
||||
let v = ZStack(alignment: .bottomTrailing) {
|
||||
@@ -40,10 +42,10 @@ struct CIGroupInvitationView: View {
|
||||
.overlay(DetermineWidth())
|
||||
(
|
||||
Text(chatIncognito ? "Tap to join incognito" : "Tap to join")
|
||||
.foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : .accentColor)
|
||||
.foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : theme.colors.primary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
|
||||
)
|
||||
.overlay(DetermineWidth())
|
||||
}
|
||||
@@ -51,7 +53,7 @@ struct CIGroupInvitationView: View {
|
||||
(
|
||||
groupInvitationText()
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
|
||||
)
|
||||
.overlay(DetermineWidth())
|
||||
}
|
||||
@@ -63,11 +65,11 @@ struct CIGroupInvitationView: View {
|
||||
}
|
||||
}
|
||||
|
||||
CIMetaView(chat: chat, chatItem: chatItem, showStatus: false, showEdited: false)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary, showStatus: false, showEdited: false)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(chatItemFrameColor(chatItem, colorScheme))
|
||||
.background(chatItemFrameColor(chatItem, theme))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
|
||||
@@ -97,7 +99,7 @@ struct CIGroupInvitationView: View {
|
||||
private func groupInfoView(_ action: Bool) -> some View {
|
||||
var color: Color
|
||||
if action && !inProgress {
|
||||
color = chatIncognito ? .indigo : .accentColor
|
||||
color = chatIncognito ? .indigo : theme.colors.primary
|
||||
} else {
|
||||
color = Color(uiColor: .tertiaryLabel)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ struct CIImageView: View {
|
||||
let maxWidth: CGFloat
|
||||
@Binding var imgWidth: CGFloat?
|
||||
@State var scrollProxy: ScrollViewProxy?
|
||||
@State var metaColor: Color
|
||||
@State private var showFullScreenImage = false
|
||||
|
||||
var body: some View {
|
||||
@@ -38,7 +37,7 @@ struct CIImageView: View {
|
||||
.onTapGesture {
|
||||
if let file = file {
|
||||
switch file.fileStatus {
|
||||
case .rcvInvitation:
|
||||
case .rcvInvitation, .rcvAborted:
|
||||
Task {
|
||||
if let user = m.currentUser {
|
||||
await receiveFile(user: user, fileId: file.fileId)
|
||||
@@ -61,6 +60,26 @@ struct CIImageView: View {
|
||||
case .rcvTransfer: () // ?
|
||||
case .rcvComplete: () // ?
|
||||
case .rcvCancelled: () // TODO
|
||||
case let .rcvError(rcvFileError):
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
case let .rcvWarning(rcvFileError):
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
case let .sndError(sndFileError):
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
case let .sndWarning(sndFileError):
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
default: ()
|
||||
}
|
||||
}
|
||||
@@ -100,13 +119,16 @@ struct CIImageView: View {
|
||||
case .sndComplete: fileIcon("checkmark", 10, 13)
|
||||
case .sndCancelled: fileIcon("xmark", 10, 13)
|
||||
case .sndError: fileIcon("xmark", 10, 13)
|
||||
case .sndWarning: fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||
case .rcvInvitation: fileIcon("arrow.down", 10, 13)
|
||||
case .rcvAccepted: fileIcon("ellipsis", 14, 11)
|
||||
case .rcvTransfer: progressView()
|
||||
case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
|
||||
case .rcvComplete: EmptyView()
|
||||
case .rcvCancelled: fileIcon("xmark", 10, 13)
|
||||
case .rcvError: fileIcon("xmark", 10, 13)
|
||||
case .rcvWarning: fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||
case .invalid: fileIcon("questionmark", 10, 13)
|
||||
default: EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,7 +138,7 @@ struct CIImageView: View {
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(metaColor)
|
||||
.foregroundColor(.white)
|
||||
.padding(padding)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ struct CIInvalidJSONView: View {
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
.onTapGesture { showJSON = true }
|
||||
.sheet(isPresented: $showJSON) {
|
||||
.appSheet(isPresented: $showJSON) {
|
||||
invalidJSONView(json)
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ func invalidJSONView(_ json: String) -> some View {
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding()
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
struct CIInvalidJSONView_Previews: PreviewProvider {
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CIMemberCreatedContactView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
|
||||
var body: some View {
|
||||
@@ -43,7 +44,7 @@ struct CIMemberCreatedContactView: View {
|
||||
r = r
|
||||
+ Text(openText)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
+ Text(" ")
|
||||
}
|
||||
r = r + chatItem.timestampText
|
||||
|
||||
@@ -11,12 +11,15 @@ import SimpleXChat
|
||||
|
||||
struct CIMetaView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
var metaColor = Color.secondary
|
||||
var metaColor: Color
|
||||
var paleMetaColor = Color(UIColor.tertiaryLabel)
|
||||
var showStatus = true
|
||||
var showEdited = true
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
|
||||
|
||||
var body: some View {
|
||||
if chatItem.isDeletedContent {
|
||||
chatItem.timestampText.font(.caption).foregroundColor(metaColor)
|
||||
@@ -27,24 +30,24 @@ struct CIMetaView: View {
|
||||
switch meta.itemStatus {
|
||||
case let .sndSent(sndProgress):
|
||||
switch sndProgress {
|
||||
case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited)
|
||||
case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited)
|
||||
case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
case let .sndRcvd(_, sndProgress):
|
||||
switch sndProgress {
|
||||
case .complete:
|
||||
ZStack {
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
case .partial:
|
||||
ZStack {
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
}
|
||||
default:
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited)
|
||||
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,10 +64,12 @@ func ciMetaText(
|
||||
chatTTL: Int?,
|
||||
encrypted: Bool?,
|
||||
color: Color = .clear,
|
||||
primaryColor: Color = .accentColor,
|
||||
transparent: Bool = false,
|
||||
sent: SentCheckmark? = nil,
|
||||
showStatus: Bool = true,
|
||||
showEdited: Bool = true
|
||||
showEdited: Bool = true,
|
||||
showViaProxy: Bool
|
||||
) -> Text {
|
||||
var r = Text("")
|
||||
if showEdited, meta.itemEdited {
|
||||
@@ -78,8 +83,11 @@ func ciMetaText(
|
||||
}
|
||||
r = r + Text(" ")
|
||||
}
|
||||
if showViaProxy, meta.sentViaProxy == true {
|
||||
r = r + statusIconText("arrow.forward", color.opacity(0.67)).font(.caption2)
|
||||
}
|
||||
if showStatus {
|
||||
if let (icon, statusColor) = meta.statusIcon(color) {
|
||||
if let (icon, statusColor) = meta.statusIcon(color, primaryColor) {
|
||||
let t = Text(Image(systemName: icon)).font(.caption2)
|
||||
let gap = Text(" ").kerning(-1.25)
|
||||
let t1 = t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67))
|
||||
@@ -106,15 +114,16 @@ 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)))
|
||||
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())
|
||||
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)
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 100))
|
||||
}
|
||||
|
||||
@@ -13,12 +13,15 @@ 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
|
||||
var chatItem: ChatItem
|
||||
@State private var alert: CIRcvDecryptionErrorAlert?
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
|
||||
|
||||
enum CIRcvDecryptionErrorAlert: Identifiable {
|
||||
case syncAllowedAlert(_ syncConnection: () -> Void)
|
||||
case syncNotSupportedContactAlert
|
||||
@@ -112,18 +115,18 @@ struct CIRcvDecryptionError: View {
|
||||
}
|
||||
(
|
||||
Text(Image(systemName: "exclamationmark.arrow.triangle.2.circlepath"))
|
||||
.foregroundColor(syncSupported ? .accentColor : .secondary)
|
||||
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ Text("Fix connection")
|
||||
.foregroundColor(syncSupported ? .accentColor : .secondary)
|
||||
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.onTapGesture(perform: { onClick() })
|
||||
@@ -140,10 +143,10 @@ struct CIRcvDecryptionError: View {
|
||||
.foregroundColor(.red)
|
||||
.italic()
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.onTapGesture(perform: { onClick() })
|
||||
|
||||
@@ -69,7 +69,7 @@ struct CIVideoView: View {
|
||||
.onTapGesture {
|
||||
if let file = file {
|
||||
switch file.fileStatus {
|
||||
case .rcvInvitation:
|
||||
case .rcvInvitation, .rcvAborted:
|
||||
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
|
||||
case .rcvAccepted:
|
||||
switch file.fileProtocol {
|
||||
@@ -95,7 +95,7 @@ struct CIVideoView: View {
|
||||
}
|
||||
durationProgress()
|
||||
}
|
||||
if let file = file, case .rcvInvitation = file.fileStatus {
|
||||
if let file = file, showDownloadButton(file.fileStatus) {
|
||||
Button {
|
||||
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
|
||||
} label: {
|
||||
@@ -105,6 +105,14 @@ struct CIVideoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func showDownloadButton(_ fileStatus: CIFileStatus) -> Bool {
|
||||
switch fileStatus {
|
||||
case .rcvInvitation: true
|
||||
case .rcvAborted: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
|
||||
private func videoViewEncrypted(_ file: CIFile, _ defaultPreview: UIImage, _ duration: Int) -> some View {
|
||||
return ZStack(alignment: .topTrailing) {
|
||||
ZStack(alignment: .center) {
|
||||
@@ -184,7 +192,7 @@ struct CIVideoView: View {
|
||||
.disabled(!canBePlayed)
|
||||
}
|
||||
}
|
||||
loadingIndicator()
|
||||
fileStatusIcon()
|
||||
}
|
||||
.onAppear {
|
||||
addObserver(player, url)
|
||||
@@ -250,11 +258,11 @@ struct CIVideoView: View {
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: w)
|
||||
loadingIndicator()
|
||||
fileStatusIcon()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func loadingIndicator() -> some View {
|
||||
@ViewBuilder private func fileStatusIcon() -> some View {
|
||||
if let file = chatItem.file {
|
||||
switch file.fileStatus {
|
||||
case .sndStored:
|
||||
@@ -271,7 +279,22 @@ struct CIVideoView: View {
|
||||
}
|
||||
case .sndComplete: fileIcon("checkmark", 10, 13)
|
||||
case .sndCancelled: fileIcon("xmark", 10, 13)
|
||||
case .sndError: fileIcon("xmark", 10, 13)
|
||||
case let .sndError(sndFileError):
|
||||
fileIcon("xmark", 10, 13)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
}
|
||||
case let .sndWarning(sndFileError):
|
||||
fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
}
|
||||
case .rcvInvitation: fileIcon("arrow.down", 10, 13)
|
||||
case .rcvAccepted: fileIcon("ellipsis", 14, 11)
|
||||
case let .rcvTransfer(rcvProgress, rcvTotal):
|
||||
@@ -280,10 +303,26 @@ struct CIVideoView: View {
|
||||
} else {
|
||||
progressView()
|
||||
}
|
||||
case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
|
||||
case .rcvComplete: EmptyView()
|
||||
case .rcvCancelled: fileIcon("xmark", 10, 13)
|
||||
case .rcvError: fileIcon("xmark", 10, 13)
|
||||
case let .rcvError(rcvFileError):
|
||||
fileIcon("xmark", 10, 13)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
}
|
||||
case let .rcvWarning(rcvFileError):
|
||||
fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
}
|
||||
case .invalid: fileIcon("questionmark", 10, 13)
|
||||
default: EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,10 +357,10 @@ struct CIVideoView: View {
|
||||
}
|
||||
|
||||
// TODO encrypt: where file size is checked?
|
||||
private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool) async -> Void) {
|
||||
private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool, Bool) async -> Void) {
|
||||
Task {
|
||||
if let user = m.currentUser {
|
||||
await receiveFile(user, file.fileId, false)
|
||||
await receiveFile(user, file.fileId, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CIVoiceView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
let recordingFile: CIFile?
|
||||
let duration: Int
|
||||
@@ -92,7 +93,7 @@ struct CIVoiceView: View {
|
||||
}
|
||||
|
||||
private func metaView() -> some View {
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +119,7 @@ struct VoiceMessagePlayerTime: View {
|
||||
|
||||
struct VoiceMessagePlayer: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
var recordingFile: CIFile?
|
||||
var recordingTime: TimeInterval
|
||||
@@ -134,17 +135,53 @@ struct VoiceMessagePlayer: View {
|
||||
ZStack {
|
||||
if let recordingFile = recordingFile {
|
||||
switch recordingFile.fileStatus {
|
||||
case .sndStored: playbackButton()
|
||||
case .sndTransfer: playbackButton()
|
||||
case .sndStored:
|
||||
if recordingFile.fileProtocol == .local {
|
||||
playbackButton()
|
||||
} else {
|
||||
loadingIcon()
|
||||
}
|
||||
case .sndTransfer: loadingIcon()
|
||||
case .sndComplete: playbackButton()
|
||||
case .sndCancelled: playbackButton()
|
||||
case .sndError: playbackButton()
|
||||
case .rcvInvitation: downloadButton(recordingFile)
|
||||
case let .sndError(sndFileError):
|
||||
fileStatusIcon("multiply", 14)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
}
|
||||
case let .sndWarning(sndFileError):
|
||||
fileStatusIcon("exclamationmark.triangle.fill", 16)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(sndFileError.errorInfo)
|
||||
))
|
||||
}
|
||||
case .rcvInvitation: downloadButton(recordingFile, "play.fill")
|
||||
case .rcvAccepted: loadingIcon()
|
||||
case .rcvTransfer: loadingIcon()
|
||||
case .rcvAborted: downloadButton(recordingFile, "exclamationmark.arrow.circlepath")
|
||||
case .rcvComplete: playbackButton()
|
||||
case .rcvCancelled: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
|
||||
case .rcvError: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
|
||||
case let .rcvError(rcvFileError):
|
||||
fileStatusIcon("multiply", 14)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("File error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
}
|
||||
case let .rcvWarning(rcvFileError):
|
||||
fileStatusIcon("exclamationmark.triangle.fill", 16)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Temporary file error"),
|
||||
message: Text(rcvFileError.errorInfo)
|
||||
))
|
||||
}
|
||||
case .invalid: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
|
||||
}
|
||||
} else {
|
||||
@@ -208,7 +245,7 @@ struct VoiceMessagePlayer: View {
|
||||
.foregroundColor(color)
|
||||
.padding(.leading, image == "play.fill" ? 4 : 0)
|
||||
.frame(width: 56, height: 56)
|
||||
.background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear)
|
||||
.background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear)
|
||||
.clipShape(Circle())
|
||||
if recordingTime > 0 {
|
||||
ProgressCircle(length: recordingTime, progress: $playbackTime)
|
||||
@@ -217,7 +254,7 @@ struct VoiceMessagePlayer: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func downloadButton(_ recordingFile: CIFile) -> some View {
|
||||
private func downloadButton(_ recordingFile: CIFile, _ icon: String) -> some View {
|
||||
Button {
|
||||
Task {
|
||||
if let user = chatModel.currentUser {
|
||||
@@ -225,11 +262,12 @@ struct VoiceMessagePlayer: View {
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
playPauseIcon("play.fill")
|
||||
playPauseIcon(icon)
|
||||
}
|
||||
}
|
||||
|
||||
private struct ProgressCircle: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var length: TimeInterval
|
||||
@Binding var progress: TimeInterval?
|
||||
|
||||
@@ -237,7 +275,7 @@ struct VoiceMessagePlayer: View {
|
||||
Circle()
|
||||
.trim(from: 0, to: ((progress ?? TimeInterval(0)) / length))
|
||||
.stroke(
|
||||
Color.accentColor,
|
||||
theme.colors.primary,
|
||||
style: StrokeStyle(lineWidth: 3)
|
||||
)
|
||||
.rotationEffect(.degrees(-90))
|
||||
@@ -245,11 +283,22 @@ struct VoiceMessagePlayer: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func fileStatusIcon(_ image: String, _ size: CGFloat) -> some View {
|
||||
Image(systemName: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
||||
.frame(width: 56, height: 56)
|
||||
.background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
private func loadingIcon() -> some View {
|
||||
ProgressView()
|
||||
.frame(width: 30, height: 30)
|
||||
.frame(width: 56, height: 56)
|
||||
.background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear)
|
||||
.background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct DeletedItemView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@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)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(chatItemFrameColor(chatItem, colorScheme))
|
||||
.background(chatItemFrameColor(chatItem, theme))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct EmojiItemView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
|
||||
var body: some View {
|
||||
@@ -18,7 +19,7 @@ struct EmojiItemView: View {
|
||||
emojiText(chatItem.content.text)
|
||||
.padding(.top, 8)
|
||||
.padding(.horizontal, 6)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
.padding(.bottom, 8)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
|
||||
@@ -9,16 +9,9 @@
|
||||
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
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
@Binding var revealed: Bool
|
||||
@@ -83,16 +76,24 @@ struct FramedItemView: View {
|
||||
.accessibilityLabel("")
|
||||
}
|
||||
}
|
||||
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, colorScheme))
|
||||
.onAppear {
|
||||
metaColor = metaColor == .secondary ? theme.colors.secondary : metaColor
|
||||
}
|
||||
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, theme))
|
||||
.cornerRadius(18)
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
|
||||
|
||||
switch chatItem.meta.itemStatus {
|
||||
case .sndErrorAuth:
|
||||
v.onTapGesture { msgDeliveryError("Most likely this contact has deleted the connection with you.") }
|
||||
case let .sndError(agentError):
|
||||
v.onTapGesture { msgDeliveryError("Unexpected error: \(agentError)") }
|
||||
default: v
|
||||
if let (title, text) = chatItem.meta.itemStatus.statusInfo {
|
||||
v.onTapGesture {
|
||||
AlertManager.shared.showAlert(
|
||||
Alert(
|
||||
title: Text(title),
|
||||
message: Text(text)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +111,7 @@ struct FramedItemView: View {
|
||||
} else {
|
||||
switch (chatItem.content.msgContent) {
|
||||
case let .image(text, image):
|
||||
CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy, metaColor: metaColor)
|
||||
CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy)
|
||||
.overlay(DetermineWidth())
|
||||
if text == "" && !chatItem.meta.isLive {
|
||||
Color.clear
|
||||
@@ -157,13 +158,6 @@ struct FramedItemView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func msgDeliveryError(_ err: LocalizedStringKey) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Message delivery error",
|
||||
message: err
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder func framedItemHeader(icon: String? = nil, caption: Text, pad: Bool = false) -> some View {
|
||||
let v = HStack(spacing: 6) {
|
||||
@@ -177,13 +171,13 @@ struct FramedItemView: View {
|
||||
.font(.caption)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(theme.colors.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, colorScheme))
|
||||
.background(chatItemFrameContextColor(chatItem, theme))
|
||||
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
|
||||
v.frame(maxWidth: mediaWidth, alignment: .leading)
|
||||
} else {
|
||||
@@ -235,7 +229,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, colorScheme))
|
||||
.background(chatItemFrameContextColor(chatItem, theme))
|
||||
|
||||
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
|
||||
v.frame(maxWidth: mediaWidth, alignment: .leading)
|
||||
@@ -248,7 +242,10 @@ struct FramedItemView: View {
|
||||
Group {
|
||||
if let sender = qi.getSender(membership()) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(sender).font(.caption).foregroundColor(.secondary)
|
||||
Text(sender)
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.lineLimit(1)
|
||||
ciQuotedMsgTextView(qi, lines: 2)
|
||||
}
|
||||
} else {
|
||||
@@ -362,22 +359,22 @@ func onlyImageOrVideo(_ ci: ChatItem) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func chatItemFrameColorMaybeImageOrVideo(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
|
||||
func chatItemFrameColorMaybeImageOrVideo(_ ci: ChatItem, _ theme: AppTheme) -> Color {
|
||||
onlyImageOrVideo(ci)
|
||||
? Color.clear
|
||||
: chatItemFrameColor(ci, colorScheme)
|
||||
: chatItemFrameColor(ci, theme)
|
||||
}
|
||||
|
||||
func chatItemFrameColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
|
||||
func chatItemFrameColor(_ ci: ChatItem, _ theme: AppTheme) -> Color {
|
||||
ci.chatDir.sent
|
||||
? (colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
: Color(uiColor: .tertiarySystemGroupedBackground)
|
||||
? theme.appColors.sentMessage
|
||||
: theme.appColors.receivedMessage
|
||||
}
|
||||
|
||||
func chatItemFrameContextColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
|
||||
func chatItemFrameContextColor(_ ci: ChatItem, _ theme: AppTheme) -> Color {
|
||||
ci.chatDir.sent
|
||||
? (colorScheme == .light ? sentQuoteColorLight : sentQuoteColorDark)
|
||||
: Color(uiColor: .quaternarySystemFill)
|
||||
? theme.appColors.sentQuote
|
||||
: theme.appColors.receivedQuote
|
||||
}
|
||||
|
||||
struct FramedItemView_Previews: PreviewProvider {
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct IntegrityErrorItemView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var msgError: MsgErrorType
|
||||
var chatItem: ChatItem
|
||||
|
||||
@@ -54,6 +55,7 @@ struct IntegrityErrorItemView: View {
|
||||
|
||||
struct CIMsgError: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
var onTap: () -> Void
|
||||
|
||||
@@ -62,7 +64,7 @@ struct CIMsgError: View {
|
||||
Text(chatItem.content.text)
|
||||
.foregroundColor(.red)
|
||||
.italic()
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
|
||||
@@ -11,7 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct MarkedDeletedItemView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@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, colorScheme))
|
||||
.background(chatItemFrameColor(chatItem, theme))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ struct MsgContentView: View {
|
||||
@State private var typingIdx = 0
|
||||
@State private var timer: Timer?
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
|
||||
|
||||
var body: some View {
|
||||
if meta?.isLive == true {
|
||||
msgContentView()
|
||||
@@ -81,7 +83,7 @@ struct MsgContentView: View {
|
||||
}
|
||||
|
||||
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
|
||||
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true)
|
||||
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ struct ChatItemForwardingView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
@ViewBuilder private func forwardListView() -> some View {
|
||||
@@ -59,9 +60,13 @@ struct ChatItemForwardingView: View {
|
||||
.cornerRadius(12)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.background(Color(.systemGroupedBackground))
|
||||
.modifier(ThemedBackground())
|
||||
} else {
|
||||
emptyList()
|
||||
ZStack {
|
||||
emptyList()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,13 @@ import SimpleXChat
|
||||
struct ChatItemInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var ci: ChatItem
|
||||
@Binding var chatItemInfo: ChatItemInfo?
|
||||
@State private var selection: CIInfoTab = .history
|
||||
@State private var alert: CIInfoViewAlert? = nil
|
||||
@State private var messageStatusLimited: Bool = true
|
||||
@State private var fileStatusLimited: Bool = true
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
|
||||
enum CIInfoTab {
|
||||
@@ -99,12 +101,14 @@ 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)
|
||||
@@ -112,6 +116,7 @@ struct ChatItemInfoView: View {
|
||||
Label(local ? "Saved" : "Forwarded", systemImage: "arrowshape.turn.up.forward")
|
||||
}
|
||||
.tag(CIInfoTab.forwarded)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
@@ -121,6 +126,7 @@ struct ChatItemInfoView: View {
|
||||
}
|
||||
} else {
|
||||
historyTab()
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +163,35 @@ struct ChatItemInfoView: View {
|
||||
if developerTools {
|
||||
infoRow("Database ID", "\(meta.itemId)")
|
||||
infoRow("Record updated at", localTimestamp(meta.updatedAt))
|
||||
let msv = infoRow("Message status", ci.meta.itemStatus.id)
|
||||
Group {
|
||||
if messageStatusLimited {
|
||||
msv.lineLimit(1)
|
||||
} else {
|
||||
msv
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
messageStatusLimited.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
if let file = ci.file {
|
||||
let fsv = infoRow("File status", file.fileStatus.id)
|
||||
Group {
|
||||
if fileStatusLimited {
|
||||
fsv.lineLimit(1)
|
||||
} else {
|
||||
fsv
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
fileStatusLimited.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,7 +231,7 @@ struct ChatItemInfoView: View {
|
||||
textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(chatItemFrameColor(ci, colorScheme))
|
||||
.background(chatItemFrameColor(ci, theme))
|
||||
.cornerRadius(18)
|
||||
.contextMenu {
|
||||
if itemVersion.msgContent.text != "" {
|
||||
@@ -265,7 +300,7 @@ struct ChatItemInfoView: View {
|
||||
textBubble(qi.text, qi.formattedText, qi.getSender(nil))
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(quotedMsgFrameColor(qi, colorScheme))
|
||||
.background(quotedMsgFrameColor(qi, theme))
|
||||
.cornerRadius(18)
|
||||
.contextMenu {
|
||||
if qi.text != "" {
|
||||
@@ -289,10 +324,10 @@ struct ChatItemInfoView: View {
|
||||
.frame(maxWidth: maxWidth, alignment: .leading)
|
||||
}
|
||||
|
||||
func quotedMsgFrameColor(_ qi: CIQuote, _ colorScheme: ColorScheme) -> Color {
|
||||
func quotedMsgFrameColor(_ qi: CIQuote, _ theme: AppTheme) -> Color {
|
||||
(qi.chatDir?.sent ?? false)
|
||||
? (colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
: Color(uiColor: .tertiarySystemGroupedBackground)
|
||||
? theme.appColors.sentMessage
|
||||
: theme.appColors.receivedMessage
|
||||
}
|
||||
|
||||
@ViewBuilder private func forwardedFromTab(_ forwardedFromItem: AChatItem) -> some View {
|
||||
@@ -383,7 +418,7 @@ struct ChatItemInfoView: View {
|
||||
let mss = membersStatuses(memberDeliveryStatuses)
|
||||
if !mss.isEmpty {
|
||||
ForEach(mss, id: \.0.groupMemberId) { memberStatus in
|
||||
memberDeliveryStatusView(memberStatus.0, memberStatus.1)
|
||||
memberDeliveryStatusView(memberStatus.0, memberStatus.1, memberStatus.2)
|
||||
}
|
||||
} else {
|
||||
Text("No delivery information")
|
||||
@@ -392,25 +427,29 @@ struct ChatItemInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus)] {
|
||||
private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus, Bool?)] {
|
||||
memberDeliveryStatuses.compactMap({ mds in
|
||||
if let mem = chatModel.getGroupMember(mds.groupMemberId) {
|
||||
return (mem.wrapped, mds.memberDeliveryStatus)
|
||||
return (mem.wrapped, mds.memberDeliveryStatus, mds.sentViaProxy)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus) -> some View {
|
||||
private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus, _ sentViaProxy: Bool?) -> some View {
|
||||
HStack{
|
||||
ProfileImage(imageStr: member.image, size: 30)
|
||||
.padding(.trailing, 2)
|
||||
Text(member.chatViewName)
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
if sentViaProxy == true {
|
||||
Image(systemName: "arrow.forward")
|
||||
.foregroundColor(.secondary).opacity(0.67)
|
||||
}
|
||||
let v = Group {
|
||||
if let (icon, statusColor) = status.statusIcon(Color.secondary) {
|
||||
if let (icon, statusColor) = status.statusIcon(theme.colors.secondary, theme.colors.primary) {
|
||||
switch status {
|
||||
case .sndRcvd:
|
||||
ZStack(alignment: .trailing) {
|
||||
@@ -469,8 +508,12 @@ struct ChatItemInfoView: View {
|
||||
if developerTools {
|
||||
shareText += [
|
||||
String.localizedStringWithFormat(NSLocalizedString("Database ID: %d", comment: "copied message info"), meta.itemId),
|
||||
String.localizedStringWithFormat(NSLocalizedString("Record updated at: %@", comment: "copied message info"), localTimestamp(meta.updatedAt))
|
||||
String.localizedStringWithFormat(NSLocalizedString("Record updated at: %@", comment: "copied message info"), localTimestamp(meta.updatedAt)),
|
||||
String.localizedStringWithFormat(NSLocalizedString("Message status: %@", comment: "copied message info"), meta.itemStatus.id)
|
||||
]
|
||||
if let file = ci.file {
|
||||
shareText += [String.localizedStringWithFormat(NSLocalizedString("File status: %@", comment: "copied message info"), file.fileStatus.id)]
|
||||
}
|
||||
}
|
||||
if let qi = ci.quotedItem {
|
||||
shareText += ["", NSLocalizedString("## In reply to", comment: "copied message info")]
|
||||
|
||||
@@ -14,7 +14,7 @@ private let memberImageSize: CGFloat = 34
|
||||
|
||||
struct ChatView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@State var theme: AppTheme = buildTheme()
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@@ -60,7 +60,16 @@ struct ChatView: View {
|
||||
Divider()
|
||||
}
|
||||
ZStack(alignment: .trailing) {
|
||||
let wallpaperImage = theme.wallpaper.type.image
|
||||
let wallpaperType = theme.wallpaper.type
|
||||
let backgroundColor = theme.wallpaper.background ?? wallpaperType.defaultBackgroundColor(theme.base, theme.colors.background)
|
||||
let tintColor = theme.wallpaper.tint ?? wallpaperType.defaultTintColor(theme.base)
|
||||
chatItemsList()
|
||||
.if(wallpaperImage != nil) { view in
|
||||
view.modifier(
|
||||
ChatViewBackground(image: wallpaperImage!, imageType: wallpaperType, background: backgroundColor, tint: tintColor)
|
||||
)
|
||||
}
|
||||
if let proxy = scrollProxy {
|
||||
floatingButtons(proxy)
|
||||
}
|
||||
@@ -78,7 +87,9 @@ struct ChatView: View {
|
||||
}
|
||||
.padding(.top, 1)
|
||||
.navigationTitle(cInfo.chatViewName)
|
||||
.background(theme.colors.background)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.environmentObject(theme)
|
||||
.onAppear {
|
||||
initChatView()
|
||||
}
|
||||
@@ -89,6 +100,7 @@ struct ChatView: View {
|
||||
chat = c
|
||||
}
|
||||
initChatView()
|
||||
theme = buildTheme()
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
@@ -131,7 +143,7 @@ struct ChatView: View {
|
||||
} label: {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
}
|
||||
.sheet(isPresented: $showChatInfoSheet, onDismiss: {
|
||||
.appSheet(isPresented: $showChatInfoSheet, onDismiss: {
|
||||
connectionStats = nil
|
||||
customUserProfile = nil
|
||||
connectionCode = nil
|
||||
@@ -384,7 +396,7 @@ struct ChatView: View {
|
||||
circleButton {
|
||||
unreadCountText(unreadAbove)
|
||||
.font(.callout)
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.onTapGesture { scrollUp(proxy) }
|
||||
.contextMenu {
|
||||
@@ -404,13 +416,13 @@ struct ChatView: View {
|
||||
circleButton {
|
||||
unreadCountText(counts.unreadBelow)
|
||||
.font(.callout)
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.onTapGesture { scrollToBottom(proxy) }
|
||||
} else if counts.totalBelow > 16 {
|
||||
circleButton {
|
||||
Image(systemName: "chevron.down")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.onTapGesture { scrollToBottom(proxy) }
|
||||
}
|
||||
@@ -513,7 +525,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
|
||||
ChatItemWithMenu(
|
||||
chat: chat,
|
||||
@@ -528,7 +540,7 @@ struct ChatView: View {
|
||||
|
||||
private struct ChatItemWithMenu: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var maxWidth: CGFloat
|
||||
@@ -593,11 +605,12 @@ struct ChatView: View {
|
||||
Text(memberNames(member, prevMember, memCount))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
.padding(.leading, memberImageSize + 14)
|
||||
.padding(.top, 7)
|
||||
}
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
ProfileImage(imageStr: member.memberProfile.image, size: memberImageSize)
|
||||
ProfileImage(imageStr: member.memberProfile.image, size: memberImageSize, backgroundColor: theme.colors.background)
|
||||
.onTapGesture {
|
||||
if chatView.membersLoaded {
|
||||
selectedMember = m.getGroupMember(member.groupMemberId)
|
||||
@@ -661,7 +674,8 @@ struct ChatView: View {
|
||||
playbackState: $playbackState,
|
||||
playbackTime: $playbackTime
|
||||
)
|
||||
.uiKitContextMenu(hasImageOrVideo: ci.content.msgContent?.isImageOrVideo == true, maxWidth: maxWidth, itemWidth: $itemWidth, menu: uiMenu, allowMenu: $allowMenu)
|
||||
.environmentObject(theme)
|
||||
.uiKitContextMenu(hasImageOrVideo: ci.content.msgContent?.isImageOrVideo == true, maxWidth: maxWidth, itemWidth: $itemWidth, menu: uiMenu, allowMenu: $allowMenu, backgroundColor: theme.colors.background)
|
||||
.accessibilityLabel("")
|
||||
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
|
||||
chatItemReactions(ci)
|
||||
@@ -728,7 +742,7 @@ struct ChatView: View {
|
||||
Text("\(r.totalReacted)")
|
||||
.font(.caption)
|
||||
.fontWeight(r.userReacted ? .bold : .light)
|
||||
.foregroundColor(r.userReacted ? .accentColor : .secondary)
|
||||
.foregroundColor(r.userReacted ? theme.colors.primary : theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 6)
|
||||
@@ -1218,6 +1232,27 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func buildTheme() -> AppTheme {
|
||||
if let cId = ChatModel.shared.chatId, let chat = ChatModel.shared.getChat(cId) {
|
||||
let perChatTheme = if case let .direct(contact) = chat.chatInfo {
|
||||
contact.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight)
|
||||
} else if case let .group(groupInfo) = chat.chatInfo {
|
||||
groupInfo.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight)
|
||||
} else {
|
||||
nil as ThemeModeOverride?
|
||||
}
|
||||
let overrides = if perChatTheme != nil {
|
||||
ThemeManager.currentColors(nil, perChatTheme, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
|
||||
} else {
|
||||
nil as ThemeManager.ActiveTheme?
|
||||
}
|
||||
let theme = overrides ?? CurrentColors
|
||||
return AppTheme(name: theme.name, base: theme.base, colors: theme.colors, appColors: theme.appColors, wallpaper: theme.wallpaper)
|
||||
} else {
|
||||
return AppTheme.shared
|
||||
}
|
||||
}
|
||||
|
||||
struct ToggleNtfsButton: View {
|
||||
@ObservedObject var chat: Chat
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ComposeFileView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
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(colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
.background(theme.appColors.sentMessage)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ComposeImageView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
let images: [String]
|
||||
let cancelImage: (() -> Void)
|
||||
let cancelEnabled: Bool
|
||||
@@ -48,7 +48,7 @@ struct ComposeImageView: View {
|
||||
}
|
||||
.padding(.vertical, 1)
|
||||
.padding(.trailing, 12)
|
||||
.background(colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
.background(theme.appColors.sentMessage)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) {
|
||||
}
|
||||
|
||||
struct ComposeLinkView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
let linkPreview: LinkPreview?
|
||||
var cancelPreview: (() -> Void)? = nil
|
||||
let cancelEnabled: Bool
|
||||
@@ -62,7 +62,7 @@ struct ComposeLinkView: View {
|
||||
}
|
||||
.padding(.vertical, 1)
|
||||
.padding(.trailing, 12)
|
||||
.background(colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
.background(theme.appColors.sentMessage)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
@@ -254,6 +254,7 @@ 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
|
||||
@@ -353,10 +354,10 @@ struct ComposeView: View {
|
||||
keyboardVisible: $keyboardVisible,
|
||||
sendButtonColor: chat.chatInfo.incognito
|
||||
? .indigo.opacity(colorScheme == .dark ? 1 : 0.7)
|
||||
: .accentColor
|
||||
: theme.colors.primary
|
||||
)
|
||||
.padding(.trailing, 12)
|
||||
.background(.background)
|
||||
.background(theme.colors.background)
|
||||
.disabled(!chat.userCanSend)
|
||||
|
||||
if chat.userIsObserver {
|
||||
@@ -712,9 +713,9 @@ struct ComposeView: View {
|
||||
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
|
||||
await sendMemberContactInvitation()
|
||||
} else if case let .forwardingItem(ci, fromChatInfo) = composeState.contextItem {
|
||||
sent = await forwardItem(ci, fromChatInfo)
|
||||
sent = await forwardItem(ci, fromChatInfo, ttl)
|
||||
if !composeState.message.isEmpty {
|
||||
sent = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: nil)
|
||||
sent = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl)
|
||||
}
|
||||
} else if case let .editingItem(ci) = composeState.contextItem {
|
||||
sent = await updateMessage(ci, live: live)
|
||||
@@ -890,13 +891,14 @@ struct ComposeView: View {
|
||||
return nil
|
||||
}
|
||||
|
||||
func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo) async -> ChatItem? {
|
||||
func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo, _ ttl: Int?) async -> ChatItem? {
|
||||
if let chatItem = await apiForwardChatItem(
|
||||
toChatType: chat.chatInfo.chatType,
|
||||
toChatId: chat.chatInfo.apiId,
|
||||
fromChatType: fromChatInfo.chatType,
|
||||
fromChatId: fromChatInfo.apiId,
|
||||
itemId: forwardedItem.id
|
||||
itemId: forwardedItem.id,
|
||||
ttl: ttl
|
||||
) {
|
||||
await MainActor.run {
|
||||
chatModel.addChatItem(chat.chatInfo, chatItem)
|
||||
|
||||
@@ -25,7 +25,7 @@ func voiceMessageTime_(_ time: TimeInterval?) -> String {
|
||||
|
||||
struct ComposeVoiceView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
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(colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
.background(theme.appColors.sentMessage)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
@@ -80,7 +80,7 @@ struct ComposeVoiceView: View {
|
||||
Button {
|
||||
startPlayback()
|
||||
} label: {
|
||||
playPauseIcon("play.fill")
|
||||
playPauseIcon("play.fill", theme.colors.primary)
|
||||
}
|
||||
Text(voiceMessageTime_(recordingTime))
|
||||
case .playing:
|
||||
@@ -88,7 +88,7 @@ struct ComposeVoiceView: View {
|
||||
audioPlayer?.pause()
|
||||
playbackState = .paused
|
||||
} label: {
|
||||
playPauseIcon("pause.fill")
|
||||
playPauseIcon("pause.fill", theme.colors.primary)
|
||||
}
|
||||
Text(voiceMessageTime_(playbackTime))
|
||||
case .paused:
|
||||
@@ -96,7 +96,7 @@ struct ComposeVoiceView: View {
|
||||
audioPlayer?.play()
|
||||
playbackState = .playing
|
||||
} label: {
|
||||
playPauseIcon("play.fill")
|
||||
playPauseIcon("play.fill", theme.colors.primary)
|
||||
}
|
||||
Text(voiceMessageTime_(playbackTime))
|
||||
}
|
||||
@@ -131,7 +131,7 @@ struct ComposeVoiceView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func playPauseIcon(_ image: String, _ color: Color = .accentColor) -> some View {
|
||||
private func playPauseIcon(_ image: String, _ color: Color) -> some View {
|
||||
Image(systemName: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
@@ -162,6 +162,7 @@ struct ComposeVoiceView: View {
|
||||
}
|
||||
|
||||
private struct ProgressBar: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var length: TimeInterval
|
||||
@Binding var progress: TimeInterval?
|
||||
|
||||
@@ -169,7 +170,7 @@ struct ComposeVoiceView: View {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.fill(Color.accentColor)
|
||||
.fill(theme.colors.primary)
|
||||
.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 {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
@@ -20,7 +20,7 @@ struct ContextInvitingContactMemberView: View {
|
||||
.padding(12)
|
||||
.frame(minHeight: 50)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
.background(theme.appColors.sentMessage)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ContextItemView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@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, colorScheme))
|
||||
.background(chatItemFrameColor(contextItem, theme))
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ 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,6 +13,7 @@ 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
|
||||
@@ -224,8 +225,7 @@ struct SendMessageView: View {
|
||||
|
||||
@ViewBuilder private func sendButtonContextMenuItems() -> some View {
|
||||
if composeState.liveMessage == nil,
|
||||
!composeState.editing,
|
||||
!composeState.forwarding {
|
||||
!composeState.editing {
|
||||
if case .noContextItem = composeState.contextItem,
|
||||
!composeState.voicePreview,
|
||||
let send = sendLiveMessage,
|
||||
@@ -248,6 +248,7 @@ struct SendMessageView: View {
|
||||
}
|
||||
|
||||
private struct RecordVoiceMessageButton: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var startVoiceMessageRecording: (() -> Void)?
|
||||
var finishVoiceMessageRecording: (() -> Void)?
|
||||
@Binding var holdingVMR: Bool
|
||||
@@ -257,7 +258,7 @@ struct SendMessageView: View {
|
||||
var body: some View {
|
||||
Button(action: {}) {
|
||||
Image(systemName: "mic.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.disabled(disabled)
|
||||
.frame(width: 29, height: 29)
|
||||
@@ -324,7 +325,7 @@ struct SendMessageView: View {
|
||||
Image(systemName: "multiply")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
.frame(width: 29, height: 29)
|
||||
@@ -341,7 +342,7 @@ struct SendMessageView: View {
|
||||
Image(systemName: "bolt.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
.frame(width: 29, height: 29)
|
||||
@@ -384,7 +385,7 @@ struct SendMessageView: View {
|
||||
}
|
||||
Task {
|
||||
_ = try? await Task.sleep(nanoseconds: liveMsgInterval)
|
||||
while composeState.liveMessage != nil {
|
||||
while await composeState.liveMessage != nil {
|
||||
await update()
|
||||
_ = try? await Task.sleep(nanoseconds: liveMsgInterval)
|
||||
}
|
||||
@@ -395,7 +396,7 @@ struct SendMessageView: View {
|
||||
private func finishVoiceMessageRecordingButton() -> some View {
|
||||
Button(action: { finishVoiceMessageRecording?() }) {
|
||||
Image(systemName: "stop.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.disabled(composeState.inProgress)
|
||||
.frame(width: 29, height: 29)
|
||||
|
||||
@@ -21,6 +21,7 @@ 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
|
||||
@@ -125,6 +126,7 @@ struct AddGroupMembersViewCommon: View {
|
||||
.onChange(of: selectedContacts) { _ in
|
||||
searchFocussed = false
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func inviteMembersButton() -> some View {
|
||||
@@ -176,7 +178,7 @@ struct AddGroupMembersViewCommon: View {
|
||||
} else {
|
||||
if checked {
|
||||
icon = "checkmark.circle.fill"
|
||||
iconColor = .accentColor
|
||||
iconColor = theme.colors.primary
|
||||
} else {
|
||||
icon = "circle"
|
||||
iconColor = Color(uiColor: .tertiaryLabel)
|
||||
|
||||
@@ -135,6 +135,7 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
@@ -210,6 +211,7 @@ 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?
|
||||
|
||||
@@ -282,7 +284,7 @@ struct GroupChatInfoView: View {
|
||||
Button {
|
||||
alert = .unblockMemberAlert(mem: member)
|
||||
} label: {
|
||||
Label("Unblock member", systemImage: "hand.raised.slash").foregroundColor(.accentColor)
|
||||
Label("Unblock member", systemImage: "hand.raised.slash").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,7 +296,7 @@ struct GroupChatInfoView: View {
|
||||
Button {
|
||||
alert = .unblockForAllAlert(mem: member)
|
||||
} label: {
|
||||
Label("Unblock for all", systemImage: "hand.raised.slash").foregroundColor(.accentColor)
|
||||
Label("Unblock for all", systemImage: "hand.raised.slash").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
@@ -333,6 +335,7 @@ struct GroupChatInfoView: View {
|
||||
creatingGroup: false
|
||||
)
|
||||
.navigationBarTitle("Group link")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
if groupLink == nil {
|
||||
@@ -350,6 +353,7 @@ struct GroupChatInfoView: View {
|
||||
groupProfile: groupInfo.groupProfile
|
||||
)
|
||||
.navigationBarTitle("Group profile")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
Label("Edit group profile", systemImage: "pencil")
|
||||
@@ -364,6 +368,7 @@ struct GroupChatInfoView: View {
|
||||
welcomeText: groupInfo.groupProfile.description ?? ""
|
||||
)
|
||||
.navigationTitle("Welcome message")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
groupInfo.groupProfile.description == nil
|
||||
@@ -518,6 +523,7 @@ func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bo
|
||||
creatingGroup: creatingGroup
|
||||
)
|
||||
.navigationBarTitle("Group preferences")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
if creatingGroup {
|
||||
|
||||
@@ -133,6 +133,7 @@ struct GroupLinkView: View {
|
||||
shouldCreate = false
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func createGroupLink() {
|
||||
|
||||
@@ -35,6 +35,7 @@ struct GroupMemberInfoView: View {
|
||||
case abortSwitchAddressAlert
|
||||
case syncConnectionForceAlert
|
||||
case planAndConnectAlert(alert: PlanAndConnectAlert)
|
||||
case queueInfo(info: String)
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey)
|
||||
|
||||
var id: String {
|
||||
@@ -49,6 +50,7 @@ struct GroupMemberInfoView: View {
|
||||
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
|
||||
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
|
||||
case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)"
|
||||
case let .queueInfo(info): return "queueInfo \(info)"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
@@ -178,6 +180,18 @@ struct GroupMemberInfoView: View {
|
||||
Section("For console") {
|
||||
infoRow("Local name", member.localDisplayName)
|
||||
infoRow("Database ID", "\(member.groupMemberId)")
|
||||
Button ("Debug delivery") {
|
||||
Task {
|
||||
do {
|
||||
let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId))
|
||||
await MainActor.run { alert = .queueInfo(info: info) }
|
||||
} catch let e {
|
||||
logger.error("apiContactQueueInfo error: \(responseError(e))")
|
||||
let a = getErrorAlert(e, "Error")
|
||||
await MainActor.run { alert = .error(title: a.title, error: a.message) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,6 +237,7 @@ struct GroupMemberInfoView: View {
|
||||
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress)
|
||||
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) })
|
||||
case let .planAndConnectAlert(alert): return planAndConnectAlert(alert, dismiss: true)
|
||||
case let .queueInfo(info): return queueInfoAlert(info)
|
||||
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
|
||||
}
|
||||
}
|
||||
@@ -232,6 +247,7 @@ struct GroupMemberInfoView: View {
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
func connectViaAddressButton(_ contactLink: String) -> some View {
|
||||
@@ -359,6 +375,7 @@ struct GroupMemberInfoView: View {
|
||||
)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("Security code")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Label(
|
||||
member.verified ? "View security code" : "Verify security code",
|
||||
|
||||
@@ -34,8 +34,7 @@ struct GroupPreferencesView: View {
|
||||
featureSection(.reactions, $preferences.reactions.enable)
|
||||
featureSection(.voice, $preferences.voice.enable, $preferences.voice.role)
|
||||
featureSection(.files, $preferences.files.enable, $preferences.files.role)
|
||||
// TODO enable simplexLinks preference in 5.8
|
||||
// featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role)
|
||||
featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role)
|
||||
featureSection(.history, $preferences.history.enable)
|
||||
|
||||
if groupInfo.canEdit {
|
||||
@@ -102,8 +101,6 @@ struct GroupPreferencesView: View {
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
// remove in v5.8
|
||||
.disabled(true)
|
||||
}
|
||||
} else {
|
||||
settingsRow(icon, color: color) {
|
||||
|
||||
@@ -66,6 +66,7 @@ struct VerifyCodeView: View {
|
||||
ScanCodeView(connectionVerified: $connectionVerified, verify: verify)
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.navigationTitle("Scan code")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Label("Scan code", systemImage: "qrcode")
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ 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
|
||||
@@ -224,7 +225,7 @@ struct ChatListNavLink: View {
|
||||
} label: {
|
||||
Label("Join", systemImage: chat.chatInfo.incognito ? "theatermasks" : "ipad.and.arrow.forward")
|
||||
}
|
||||
.tint(chat.chatInfo.incognito ? .indigo : .accentColor)
|
||||
.tint(chat.chatInfo.incognito ? .indigo : theme.colors.primary)
|
||||
}
|
||||
|
||||
@ViewBuilder private func markReadButton() -> some View {
|
||||
@@ -234,14 +235,14 @@ struct ChatListNavLink: View {
|
||||
} label: {
|
||||
Label("Read", systemImage: "checkmark")
|
||||
}
|
||||
.tint(Color.accentColor)
|
||||
.tint(theme.colors.primary)
|
||||
} else {
|
||||
Button {
|
||||
Task { await markChatUnread(chat) }
|
||||
} label: {
|
||||
Label("Unread", systemImage: "circlebadge.fill")
|
||||
}
|
||||
.tint(Color.accentColor)
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -306,7 +307,7 @@ struct ChatListNavLink: View {
|
||||
Button {
|
||||
Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) }
|
||||
} label: { Label("Accept", systemImage: "checkmark") }
|
||||
.tint(.accentColor)
|
||||
.tint(theme.colors.primary)
|
||||
Button {
|
||||
Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) }
|
||||
} label: {
|
||||
@@ -346,13 +347,16 @@ struct ChatListNavLink: View {
|
||||
} label: {
|
||||
Label("Name", systemImage: "pencil")
|
||||
}
|
||||
.tint(.accentColor)
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.sheet(isPresented: $showContactConnectionInfo) {
|
||||
if case let .contactConnection(contactConnection) = chat.chatInfo {
|
||||
ContactConnectionInfo(contactConnection: contactConnection)
|
||||
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
|
||||
.appSheet(isPresented: $showContactConnectionInfo) {
|
||||
Group {
|
||||
if case let .contactConnection(contactConnection) = chat.chatInfo {
|
||||
ContactConnectionInfo(contactConnection: contactConnection)
|
||||
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
@@ -467,7 +471,7 @@ struct ChatListNavLink: View {
|
||||
.padding(4)
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.onTapGesture { showInvalidJSON = true }
|
||||
.sheet(isPresented: $showInvalidJSON) {
|
||||
.appSheet(isPresented: $showInvalidJSON) {
|
||||
invalidJSONView(json)
|
||||
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ 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
|
||||
@@ -86,6 +87,7 @@ struct ChatListView: View {
|
||||
))
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.background(theme.colors.background)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarHidden(searchMode)
|
||||
.toolbar {
|
||||
@@ -136,7 +138,7 @@ struct ChatListView: View {
|
||||
showUnreadAndFavorites = !showUnreadAndFavorites
|
||||
} label: {
|
||||
Image(systemName: "line.3.horizontal.decrease.circle" + (showUnreadAndFavorites ? ".fill" : ""))
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,12 +156,14 @@ 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)
|
||||
}
|
||||
@@ -179,7 +183,7 @@ struct ChatListView: View {
|
||||
private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View {
|
||||
Circle()
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
|
||||
private func onboardingButtons() -> some View {
|
||||
@@ -190,7 +194,7 @@ struct ChatListView: View {
|
||||
p.addLine(to: CGPoint(x: 0, y: 10))
|
||||
p.addLine(to: CGPoint(x: 8, y: 0))
|
||||
}
|
||||
.fill(Color.accentColor)
|
||||
.fill(theme.colors.primary)
|
||||
.frame(width: 20, height: 10)
|
||||
.padding(.trailing, 12)
|
||||
|
||||
@@ -213,7 +217,7 @@ struct ChatListView: View {
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.background(Color.accentColor)
|
||||
.background(theme.colors.primary)
|
||||
.foregroundColor(.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
@@ -276,6 +280,7 @@ struct ChatListView: View {
|
||||
|
||||
struct ChatListSearchBar: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var searchMode: Bool
|
||||
@FocusState.Binding var searchFocussed: Bool
|
||||
@Binding var searchText: String
|
||||
@@ -330,7 +335,7 @@ struct ChatListSearchBar: View {
|
||||
|
||||
if searchFocussed {
|
||||
Text("Cancel")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.onTapGesture {
|
||||
searchText = ""
|
||||
searchFocussed = false
|
||||
|
||||
@@ -11,6 +11,7 @@ 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
|
||||
@@ -94,7 +95,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 : .accentColor)
|
||||
case .memInvited: v.foregroundColor(deleting ? .secondary : chat.chatInfo.incognito ? .indigo : theme.colors.primary)
|
||||
case .memAccepted: v.foregroundColor(.secondary)
|
||||
default: if deleting { v.foregroundColor(.secondary) } else { v }
|
||||
}
|
||||
@@ -133,7 +134,7 @@ struct ChatPreviewView: View {
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 4)
|
||||
.frame(minWidth: 18, minHeight: 18)
|
||||
.background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? Color.accentColor : Color.secondary)
|
||||
.background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? theme.colors.primary : theme.colors.secondary)
|
||||
.cornerRadius(10)
|
||||
} else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local {
|
||||
Image(systemName: "speaker.slash.fill")
|
||||
@@ -151,7 +152,7 @@ struct ChatPreviewView: View {
|
||||
|
||||
private func messageDraft(_ draft: ComposeState) -> Text {
|
||||
let msg = draft.message
|
||||
return image("rectangle.and.pencil.and.ellipsis", color: .accentColor)
|
||||
return image("rectangle.and.pencil.and.ellipsis", color: theme.colors.primary)
|
||||
+ attachment()
|
||||
+ messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true, showSecrets: false)
|
||||
|
||||
@@ -206,7 +207,7 @@ struct ChatPreviewView: View {
|
||||
case let .direct(contact):
|
||||
if contact.activeConn == nil && contact.profile.contactLink != nil {
|
||||
chatPreviewInfoText("Tap to Connect")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
} else if !contact.ready && contact.activeConn != nil {
|
||||
if contact.nextSendGrpInv {
|
||||
chatPreviewInfoText("send direct message")
|
||||
@@ -240,14 +241,14 @@ struct ChatPreviewView: View {
|
||||
|
||||
private func itemStatusMark(_ cItem: ChatItem) -> Text {
|
||||
switch cItem.meta.itemStatus {
|
||||
case .sndErrorAuth:
|
||||
case .sndErrorAuth, .sndError:
|
||||
return Text(Image(systemName: "multiply"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.red) + Text(" ")
|
||||
case .sndError:
|
||||
case .sndWarning:
|
||||
return Text(Image(systemName: "exclamationmark.triangle.fill"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.yellow) + Text(" ")
|
||||
.foregroundColor(.orange) + Text(" ")
|
||||
default: return Text("")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ 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?
|
||||
@@ -82,6 +83,7 @@ struct ContactConnectionInfo: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
if #available(iOS 16, *) {
|
||||
v
|
||||
} else {
|
||||
@@ -149,7 +151,7 @@ struct ContactConnectionInfo: View {
|
||||
HStack(spacing: 6) {
|
||||
Text("Incognito")
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.onTapGesture {
|
||||
@@ -178,6 +180,7 @@ private func oneTimeLinkLearnMoreButton() -> some View {
|
||||
NavigationLink {
|
||||
AddContactLearnMore(showTitle: false)
|
||||
.navigationTitle("One-time invitation link")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
settingsRow("info.circle") {
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct ContactRequestView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var contactRequest: UserContactRequest
|
||||
@ObservedObject var chat: Chat
|
||||
|
||||
@@ -23,7 +24,7 @@ struct ContactRequestView: View {
|
||||
Text(contactRequest.chatViewName)
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.padding(.leading, 8)
|
||||
.frame(alignment: .topLeading)
|
||||
Spacer()
|
||||
|
||||
@@ -6,13 +6,11 @@
|
||||
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
|
||||
@@ -21,9 +19,6 @@ 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 {
|
||||
@@ -82,7 +77,7 @@ struct UserPicker: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.background(
|
||||
Rectangle()
|
||||
.fill(fillColor)
|
||||
.fill(theme.colors.surface)
|
||||
.cornerRadius(16)
|
||||
.shadow(color: .black.opacity(0.12), radius: 24, x: 0, y: 0)
|
||||
)
|
||||
@@ -137,7 +132,7 @@ struct UserPicker: View {
|
||||
if user.activeUser {
|
||||
Image(systemName: "checkmark")
|
||||
} else if u.unreadCount > 0 {
|
||||
unreadCounter(u.unreadCount, color: user.showNtfs ? .accentColor : .secondary)
|
||||
unreadCounter(u.unreadCount, color: user.showNtfs ? theme.colors.primary : theme.colors.secondary)
|
||||
} else if !user.showNtfs {
|
||||
Image(systemName: "speaker.slash")
|
||||
}
|
||||
@@ -145,7 +140,7 @@ struct UserPicker: View {
|
||||
.padding(.trailing)
|
||||
.padding([.leading, .vertical], 12)
|
||||
})
|
||||
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
|
||||
.buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill)))
|
||||
}
|
||||
|
||||
private func menuButton(_ title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View {
|
||||
@@ -162,7 +157,7 @@ struct UserPicker: View {
|
||||
.padding(.vertical, 22)
|
||||
.frame(height: menuButtonHeight)
|
||||
}
|
||||
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
|
||||
.buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@ struct DatabaseView: View {
|
||||
NavigationLink {
|
||||
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false)
|
||||
.navigationTitle("Database passphrase")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text("Database passphrase")
|
||||
}
|
||||
@@ -144,6 +145,7 @@ struct DatabaseView: View {
|
||||
NavigationLink {
|
||||
ChatArchiveView(archiveName: archiveName)
|
||||
.navigationTitle(title)
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text(title)
|
||||
}
|
||||
@@ -355,6 +357,7 @@ 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,6 +189,7 @@ 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 {
|
||||
@@ -231,6 +232,7 @@ 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 {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
var size: CGFloat
|
||||
var color = Color(uiColor: .tertiarySystemGroupedBackground)
|
||||
@@ -24,8 +24,7 @@ struct ChatInfoImage: View {
|
||||
case .contactRequest: iconName = "person.crop.circle.fill"
|
||||
default: iconName = "circle.fill"
|
||||
}
|
||||
let notesColor = colorScheme == .light ? notesChatColorLight : notesChatColorDark
|
||||
let iconColor = if case .local = chat.chatInfo { notesColor } else { color }
|
||||
let iconColor = if case .local = chat.chatInfo { theme.appColors.primaryVariant2 } else { color }
|
||||
return ProfileImage(
|
||||
imageStr: chat.chatInfo.image,
|
||||
iconName: iconName,
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// 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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import UIKit
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
func uiKitContextMenu(hasImageOrVideo: Bool, maxWidth: CGFloat, itemWidth: Binding<CGFloat>, menu: Binding<UIMenu>, allowMenu: Binding<Bool>) -> some 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 {
|
||||
@@ -19,10 +19,10 @@ extension View {
|
||||
self.environmentObject(ChatModel.shared)
|
||||
.overlay(DetermineWidthImageVideoItem())
|
||||
.onPreferenceChange(DetermineWidthImageVideoItem.Key.self) { itemWidth.wrappedValue = $0 == 0 ? maxWidth : $0 }
|
||||
, maxWidth: maxWidth, itemWidth: itemWidth, menu: menu)
|
||||
, 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)
|
||||
InteractionView(content: self.environmentObject(ChatModel.shared), maxWidth: maxWidth, itemWidth: itemWidth, menu: menu, backgroundColor: backgroundColor)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
}
|
||||
} else {
|
||||
@@ -42,11 +42,14 @@ struct InteractionView<Content: View>: UIViewRepresentable {
|
||||
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 = [
|
||||
@@ -72,6 +75,7 @@ struct InteractionView<Content: View>: UIViewRepresentable {
|
||||
if was != (uiView as! HostingViewHolder).contentSize {
|
||||
uiView.invalidateIntrinsicContentSize()
|
||||
}
|
||||
//uiView.backgroundColor = UIColor(backgroundColor)
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
|
||||
@@ -16,6 +16,7 @@ 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 {
|
||||
@@ -28,6 +29,7 @@ struct ProfileImage: View {
|
||||
.resizable()
|
||||
.foregroundColor(color)
|
||||
.frame(width: size, height: size)
|
||||
.background(backgroundColor != nil ? backgroundColor! : .clear)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// 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(Color(uiColor: .systemBackground))
|
||||
.foregroundColor(AppTheme.shared.colors.background)
|
||||
label()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ struct PasscodeView: View {
|
||||
}
|
||||
.padding(.horizontal, 40)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color(uiColor: .systemBackground))
|
||||
.background(AppTheme.shared.colors.background)
|
||||
}
|
||||
|
||||
private func verticalPasscodeView(_ g: GeometryProxy) -> some View {
|
||||
|
||||
@@ -53,6 +53,7 @@ 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
|
||||
@@ -215,7 +216,7 @@ struct MigrateFromDevice: View {
|
||||
Section {
|
||||
Button(action: { migrationState = .archiving }) {
|
||||
settingsRow("tray.and.arrow.up") {
|
||||
Text("Archive and upload").foregroundColor(.accentColor)
|
||||
Text("Archive and upload").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -249,7 +250,7 @@ struct MigrateFromDevice: View {
|
||||
}
|
||||
}
|
||||
let ratio = Float(uploadedBytes) / Float(totalBytes)
|
||||
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: uploadedBytes, countStyle: .binary)) uploaded")
|
||||
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: uploadedBytes, countStyle: .binary)) uploaded", theme.colors.primary)
|
||||
}
|
||||
.onAppear {
|
||||
startUploading(totalBytes, archivePath)
|
||||
@@ -263,7 +264,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(.accentColor)
|
||||
Text("Repeat upload").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -299,7 +300,7 @@ struct MigrateFromDevice: View {
|
||||
}
|
||||
Button(action: { finishMigration(fileId, ctrl) }) {
|
||||
settingsRow("checkmark") {
|
||||
Text("Finalize migration").foregroundColor(.accentColor)
|
||||
Text("Finalize migration").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} footer: {
|
||||
@@ -340,7 +341,7 @@ struct MigrateFromDevice: View {
|
||||
}
|
||||
Button(action: { alert = .deleteChat() }) {
|
||||
settingsRow("trash.fill") {
|
||||
Text("Delete database from this device").foregroundColor(.accentColor)
|
||||
Text("Delete database from this device").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -379,7 +380,7 @@ struct MigrateFromDevice: View {
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
|
||||
static func largeProgressView(_ value: Float, _ title: String, _ description: LocalizedStringKey) -> some View {
|
||||
static func largeProgressView(_ value: Float, _ title: String, _ description: LocalizedStringKey, _ primaryColor: Color) -> some View {
|
||||
ZStack {
|
||||
VStack {
|
||||
Text(description)
|
||||
@@ -389,7 +390,7 @@ struct MigrateFromDevice: View {
|
||||
Text(title)
|
||||
.font(.system(size: 54))
|
||||
.bold()
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(primaryColor)
|
||||
|
||||
Text(description)
|
||||
.font(.title3)
|
||||
@@ -398,7 +399,7 @@ struct MigrateFromDevice: View {
|
||||
Circle()
|
||||
.trim(from: 0, to: CGFloat(value))
|
||||
.stroke(
|
||||
Color.accentColor,
|
||||
primaryColor,
|
||||
style: StrokeStyle(lineWidth: 27)
|
||||
)
|
||||
.rotationEffect(.degrees(180))
|
||||
|
||||
@@ -91,6 +91,7 @@ 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?
|
||||
@@ -243,7 +244,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")
|
||||
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: downloadedBytes, countStyle: .binary)) downloaded", theme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +256,7 @@ struct MigrateToDevice: View {
|
||||
migrationState = .linkDownloading(link: link)
|
||||
}) {
|
||||
settingsRow("tray.and.arrow.down") {
|
||||
Text("Repeat download").foregroundColor(.accentColor)
|
||||
Text("Repeat download").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -293,7 +294,7 @@ struct MigrateToDevice: View {
|
||||
migrationState = .archiveImport(archivePath: archivePath)
|
||||
}) {
|
||||
settingsRow("square.and.arrow.down") {
|
||||
Text("Repeat import").foregroundColor(.accentColor)
|
||||
Text("Repeat import").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -334,7 +335,7 @@ struct MigrateToDevice: View {
|
||||
migrationState = .migration(passphrase: passphrase, confirmation: confirmation, useKeychain: useKeychain)
|
||||
}) {
|
||||
settingsRow("square.and.arrow.down") {
|
||||
Text(button).foregroundColor(.accentColor)
|
||||
Text(button).foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -364,6 +365,7 @@ struct MigrateToDevice: View {
|
||||
}
|
||||
|
||||
struct OnionView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@State var appSettings: AppSettings
|
||||
@State private var onionHosts: OnionHosts = .no
|
||||
var finishMigration: (AppSettings) -> Void
|
||||
@@ -381,7 +383,7 @@ struct MigrateToDevice: View {
|
||||
finishMigration(appSettings)
|
||||
}) {
|
||||
settingsRow("checkmark") {
|
||||
Text("Apply").foregroundColor(.accentColor)
|
||||
Text("Apply").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -475,6 +477,7 @@ 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,6 +30,7 @@ struct AddContactLearnMore: View {
|
||||
}
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ 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?
|
||||
@@ -95,7 +96,7 @@ struct AddGroupView: View {
|
||||
Section {
|
||||
groupNameTextField()
|
||||
Button(action: createGroup) {
|
||||
settingsRow("checkmark", color: .accentColor) { Text("Create group") }
|
||||
settingsRow("checkmark", color: theme.colors.primary) { Text("Create group") }
|
||||
}
|
||||
.disabled(!canCreateProfile())
|
||||
IncognitoToggle(incognitoEnabled: $incognitoDefault)
|
||||
@@ -144,6 +145,7 @@ struct AddGroupView: View {
|
||||
profile.image = nil
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
func groupNameTextField() -> some View {
|
||||
|
||||
@@ -94,7 +94,7 @@ struct NewChatView: View {
|
||||
.background(
|
||||
// Rectangle is needed for swipe gesture to work on mostly empty views (creatingLinkProgressView and retryButton)
|
||||
Rectangle()
|
||||
.fill(Color(uiColor: .systemGroupedBackground))
|
||||
.fill(AppTheme.shared.colors.background)
|
||||
)
|
||||
.animation(.easeInOut(duration: 0.3333), value: selection)
|
||||
.gesture(DragGesture(minimumDistance: 20.0, coordinateSpace: .local)
|
||||
@@ -113,7 +113,7 @@ struct NewChatView: View {
|
||||
}
|
||||
)
|
||||
}
|
||||
.background(Color(.systemGroupedBackground))
|
||||
.modifier(ThemedBackground())
|
||||
.onChange(of: invitationUsed) { used in
|
||||
if used && !(m.showingInvitation?.connChatUsed ?? true) {
|
||||
m.markShowingInvitationUsed()
|
||||
@@ -367,11 +367,12 @@ struct ScannerInView: View {
|
||||
@Binding var showQRCodeScanner: Bool
|
||||
let processQRCode: (_ resp: Result<ScanResult, ScanError>) -> Void
|
||||
@State private var cameraAuthorizationStatus: AVAuthorizationStatus?
|
||||
var scanMode: ScanMode = .continuous
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if showQRCodeScanner, case .authorized = cameraAuthorizationStatus {
|
||||
CodeScannerView(codeTypes: [.qr], scanMode: .continuous, completion: processQRCode)
|
||||
CodeScannerView(codeTypes: [.qr], scanMode: scanMode, completion: processQRCode)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.cornerRadius(12)
|
||||
.listRowBackground(Color.clear)
|
||||
@@ -436,6 +437,7 @@ struct ScannerInView: View {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func linkTextView(_ link: String) -> some View {
|
||||
Text(link)
|
||||
.lineLimit(1)
|
||||
@@ -486,6 +488,7 @@ func strHasSingleSimplexLink(_ str: String) -> FormattedText? {
|
||||
}
|
||||
|
||||
struct IncognitoToggle: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var incognitoEnabled: Bool
|
||||
@State private var showIncognitoSheet = false
|
||||
|
||||
@@ -499,7 +502,7 @@ struct IncognitoToggle: View {
|
||||
HStack(spacing: 6) {
|
||||
Text("Incognito")
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.onTapGesture {
|
||||
|
||||
@@ -66,6 +66,7 @@ struct CreateProfile: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Create your profile")
|
||||
.modifier(ThemedBackground())
|
||||
.alert(item: $alert) { a in userProfileAlert(a, $displayName) }
|
||||
.onAppear() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
@@ -78,6 +79,7 @@ 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
|
||||
@@ -89,9 +91,9 @@ struct CreateFirstProfile: View {
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Text("Your profile, contacts and delivered messages are stored on your device.")
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
Text("The profile is only shared with your contacts.")
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.padding(.bottom)
|
||||
|
||||
@@ -44,6 +44,7 @@ struct HowItWorks: View {
|
||||
.lineLimit(10)
|
||||
.padding()
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ struct SetNotificationsMode: View {
|
||||
}
|
||||
|
||||
struct NtfModeSelector: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var mode: NotificationsMode
|
||||
@Binding var selection: NotificationsMode
|
||||
@State private var tapped = false
|
||||
@@ -87,7 +88,7 @@ struct NtfModeSelector: View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(mode.label)
|
||||
.font(.headline)
|
||||
.foregroundColor(selection == mode ? .accentColor : .secondary)
|
||||
.foregroundColor(selection == mode ? theme.colors.primary : theme.colors.secondary)
|
||||
Text(ntfModeDescription(mode))
|
||||
.lineLimit(10)
|
||||
.font(.subheadline)
|
||||
@@ -95,11 +96,11 @@ struct NtfModeSelector: View {
|
||||
.padding(12)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(uiColor: tapped ? .secondarySystemFill : .systemBackground))
|
||||
.background(tapped ? Color(uiColor: .secondarySystemFill) : theme.colors.background)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 18))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(selection == mode ? Color.accentColor : Color(uiColor: .secondarySystemFill), lineWidth: 2)
|
||||
.stroke(selection == mode ? theme.colors.primary : Color(uiColor: .secondarySystemFill), lineWidth: 2)
|
||||
)
|
||||
._onButtonGesture { down in
|
||||
tapped = down
|
||||
|
||||
@@ -79,7 +79,7 @@ struct SimpleXInfo: View {
|
||||
MigrateToDevice(migrationState: $m.migrationState)
|
||||
}
|
||||
.navigationTitle("Migrate here")
|
||||
.background(colorScheme == .light ? Color(uiColor: .tertiarySystemGroupedBackground) : .clear)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showHowItWorks) {
|
||||
|
||||
@@ -406,7 +406,28 @@ private let versionDescriptions: [VersionDescription] = [
|
||||
description: "More reliable network connection."
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
VersionDescription(
|
||||
version: "v5.8",
|
||||
post: URL(string: "https://simplex.chat/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html"),
|
||||
features: [
|
||||
FeatureDescription(
|
||||
icon: "arrow.forward",
|
||||
title: "Private message routing 🚀",
|
||||
description: "Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings."
|
||||
),
|
||||
FeatureDescription(
|
||||
icon: "network.badge.shield.half.filled",
|
||||
title: "Safely receive files",
|
||||
description: "Confirm files from unknown servers."
|
||||
),
|
||||
FeatureDescription(
|
||||
icon: "battery.50",
|
||||
title: "Improved message delivery",
|
||||
description: "With reduced battery usage."
|
||||
)
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
private let lastVersion = versionDescriptions.last!.version
|
||||
|
||||
@@ -178,26 +178,32 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Connect to desktop")
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func connectingDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> some View {
|
||||
List {
|
||||
Section("Connecting to desktop") {
|
||||
ctrlDeviceNameText(session, rc)
|
||||
ctrlDeviceVersionText(session)
|
||||
}
|
||||
ZStack {
|
||||
List {
|
||||
Section("Connecting to desktop") {
|
||||
ctrlDeviceNameText(session, rc)
|
||||
ctrlDeviceVersionText(session)
|
||||
}
|
||||
|
||||
if let sessCode = session.sessionCode {
|
||||
Section("Session code") {
|
||||
sessionCodeText(sessCode)
|
||||
if let sessCode = session.sessionCode {
|
||||
Section("Session code") {
|
||||
sessionCodeText(sessCode)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
disconnectButton()
|
||||
}
|
||||
}
|
||||
.navigationTitle("Connecting to desktop")
|
||||
|
||||
Section {
|
||||
disconnectButton()
|
||||
}
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
.navigationTitle("Connecting to desktop")
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func searchingDesktopView() -> some View {
|
||||
@@ -215,6 +221,7 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Connecting to desktop")
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
@ViewBuilder private func foundDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo, _ compatible: Bool) -> some View {
|
||||
@@ -242,6 +249,7 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Found desktop")
|
||||
.modifier(ThemedBackground())
|
||||
|
||||
if compatible && connectRemoteViaMulticastAuto {
|
||||
v.onAppear { confirmKnownDesktop(rc) }
|
||||
@@ -271,6 +279,7 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Verify connection")
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text {
|
||||
@@ -311,6 +320,7 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Connected to desktop")
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func sessionCodeText(_ code: String) -> some View {
|
||||
@@ -332,13 +342,7 @@ struct ConnectDesktopView: View {
|
||||
|
||||
private func scanDesctopAddressView() -> some View {
|
||||
Section("Scan QR code from desktop") {
|
||||
CodeScannerView(codeTypes: [.qr], scanMode: .oncePerCode, completion: processDesktopQRCode)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.cornerRadius(12)
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
.padding(.horizontal)
|
||||
ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processDesktopQRCode, scanMode: .oncePerCode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,6 +395,7 @@ struct ConnectDesktopView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Linked desktops")
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func remoteCtrlView(_ rc: RemoteCtrlInfo) -> some View {
|
||||
|
||||
@@ -106,6 +106,7 @@ struct TerminalView: View {
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
.navigationTitle("Chat console")
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
func scrollToBottom(_ proxy: ScrollViewProxy, animation: Animation = .default) {
|
||||
@@ -130,6 +131,7 @@ struct TerminalView: View {
|
||||
}
|
||||
}
|
||||
.onDisappear { terminalItem = nil }
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
func consoleSendMessage() {
|
||||
|
||||
@@ -51,10 +51,10 @@ struct AdvancedNetworkSettings: View {
|
||||
}
|
||||
.disabled(currentNetCfg == NetCfg.proxyDefaults)
|
||||
|
||||
timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [10_000000, 15_000000, 20_000000, 25_000000, 35_000000, 50_000000], label: secondsLabel)
|
||||
timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [10_000000, 15_000000, 20_000000, 30_000000, 45_000000, 60_000000, 90_000000], label: secondsLabel)
|
||||
timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel)
|
||||
timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [2_500, 5_000, 10_000, 15_000, 20_000, 30_000], label: secondsLabel)
|
||||
intSettingPicker("Receiving concurrency", selection: $netCfg.rcvConcurrency, values: [1, 2, 4, 8, 12, 16, 24], label: "")
|
||||
// intSettingPicker("Receiving concurrency", selection: $netCfg.rcvConcurrency, values: [1, 2, 4, 8, 12, 16, 24], label: "")
|
||||
timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel)
|
||||
intSettingPicker("PING count", selection: $netCfg.smpPingCount, values: [1, 2, 3, 5, 8], label: "")
|
||||
Toggle("Enable TCP keep-alive", isOn: $enableKeepAlive)
|
||||
|
||||
@@ -23,6 +23,7 @@ extension AppSettings {
|
||||
setNetCfg(val)
|
||||
}
|
||||
if let val = privacyEncryptLocalFiles { privacyEncryptLocalFilesGroupDefault.set(val) }
|
||||
if let val = privacyAskToApproveRelays { privacyAskToApproveRelaysGroupDefault.set(val) }
|
||||
if let val = privacyAcceptImages {
|
||||
privacyAcceptImagesGroupDefault.set(val)
|
||||
def.setValue(val, forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES)
|
||||
@@ -43,6 +44,11 @@ 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 {
|
||||
@@ -50,6 +56,7 @@ extension AppSettings {
|
||||
var c = AppSettings.defaults
|
||||
c.networkConfig = getNetCfg()
|
||||
c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get()
|
||||
c.privacyAskToApproveRelays = privacyAskToApproveRelaysGroupDefault.get()
|
||||
c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get()
|
||||
c.privacyLinkPreviews = def.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS)
|
||||
c.privacyShowChatPreviews = def.bool(forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS)
|
||||
@@ -67,6 +74,11 @@ 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,12 +7,17 @@
|
||||
//
|
||||
|
||||
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 interfaceStyleNames: [LocalizedStringKey] = ["System", "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 appSettingsURL = URL(string: UIApplication.openSettingsURLString)!
|
||||
|
||||
@@ -22,7 +27,11 @@ 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 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 uiTintColor = getUIAccentColorDefault()
|
||||
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileImageCornerRadius = defaultProfileImageCorner
|
||||
|
||||
@@ -65,9 +74,15 @@ struct AppearanceSettings: View {
|
||||
}
|
||||
|
||||
Section {
|
||||
Picker("Theme", selection: $userInterfaceStyle) {
|
||||
ForEach(interfaceStyles, id: \.self) { style in
|
||||
Text(interfaceStyleNames[interfaceStyles.firstIndex(of: style) ?? 0])
|
||||
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])
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
@@ -82,9 +97,24 @@ struct AppearanceSettings: View {
|
||||
Text("Reset colors").font(.callout)
|
||||
}
|
||||
}
|
||||
.onChange(of: userInterfaceStyle) { _ in
|
||||
sceneDelegate.window?.overrideUserInterfaceStyle = userInterfaceStyle
|
||||
setUserInterfaceStyleDefault(userInterfaceStyle)
|
||||
.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: uiTintColor) { _ in
|
||||
sceneDelegate.window?.tintColor = UIColor(cgColor: uiTintColor)
|
||||
@@ -136,24 +166,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,6 +22,7 @@ struct CallSettings: View {
|
||||
NavigationLink {
|
||||
RTCServers()
|
||||
.navigationTitle("Your ICE servers")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text("WebRTC ICE servers")
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ struct HiddenProfileView: View {
|
||||
message: Text(savePasswordError ?? "")
|
||||
)
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
var passwordValid: Bool { hidePassword == hidePassword.trimmingCharacters(in: .whitespaces) }
|
||||
|
||||
@@ -27,6 +27,7 @@ struct IncognitoHelp: View {
|
||||
}
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,12 +12,16 @@ import SimpleXChat
|
||||
private enum NetworkAlert: Identifiable {
|
||||
case updateOnionHosts(hosts: OnionHosts)
|
||||
case updateSessionMode(mode: TransportSessionMode)
|
||||
case updateSMPProxyMode(proxyMode: SMPProxyMode)
|
||||
case updateSMPProxyFallback(proxyFallback: SMPProxyFallback)
|
||||
case error(err: String)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case let .updateOnionHosts(hosts): return "updateOnionHosts \(hosts)"
|
||||
case let .updateSessionMode(mode): return "updateSessionMode \(mode)"
|
||||
case let .updateSMPProxyMode(proxyMode): return "updateSMPProxyMode \(proxyMode)"
|
||||
case let .updateSMPProxyFallback(proxyFallback): return "updateSMPProxyFallback \(proxyFallback)"
|
||||
case let .error(err): return "error \(err)"
|
||||
}
|
||||
}
|
||||
@@ -26,11 +30,14 @@ 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
|
||||
@State private var cfgLoaded = false
|
||||
@State private var currentNetCfg = NetCfg.defaults
|
||||
@State private var netCfg = NetCfg.defaults
|
||||
@State private var onionHosts: OnionHosts = .no
|
||||
@State private var sessionMode: TransportSessionMode = .user
|
||||
@State private var proxyMode: SMPProxyMode = .never
|
||||
@State private var proxyFallback: SMPProxyFallback = .allow
|
||||
@State private var alert: NetworkAlert?
|
||||
|
||||
var body: some View {
|
||||
@@ -40,6 +47,7 @@ struct NetworkAndServers: View {
|
||||
NavigationLink {
|
||||
ProtocolServersView(serverProtocol: .smp)
|
||||
.navigationTitle("Your SMP servers")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text("SMP servers")
|
||||
}
|
||||
@@ -47,6 +55,7 @@ struct NetworkAndServers: View {
|
||||
NavigationLink {
|
||||
ProtocolServersView(serverProtocol: .xftp)
|
||||
.navigationTitle("Your XFTP servers")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text("XFTP servers")
|
||||
}
|
||||
@@ -66,6 +75,7 @@ struct NetworkAndServers: View {
|
||||
NavigationLink {
|
||||
AdvancedNetworkSettings()
|
||||
.navigationTitle("Network settings")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text("Advanced network settings")
|
||||
}
|
||||
@@ -75,10 +85,35 @@ struct NetworkAndServers: View {
|
||||
Text("Using .onion hosts requires compatible VPN provider.")
|
||||
}
|
||||
|
||||
Section {
|
||||
Picker("Private routing", selection: $proxyMode) {
|
||||
ForEach(SMPProxyMode.values, id: \.self) { Text($0.text) }
|
||||
}
|
||||
.frame(height: 36)
|
||||
|
||||
Picker("Allow downgrade", selection: $proxyFallback) {
|
||||
ForEach(SMPProxyFallback.values, id: \.self) { Text($0.text) }
|
||||
}
|
||||
.disabled(proxyMode == .never)
|
||||
.frame(height: 36)
|
||||
|
||||
Toggle("Show message status", isOn: $showSentViaProxy)
|
||||
} header: {
|
||||
Text("Private message routing")
|
||||
} footer: {
|
||||
VStack(alignment: .leading) {
|
||||
Text("To protect your IP address, private routing uses your SMP servers to deliver messages.")
|
||||
if showSentViaProxy {
|
||||
Text("Show → on messages sent via private routing.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Calls") {
|
||||
NavigationLink {
|
||||
RTCServers()
|
||||
.navigationTitle("Your ICE servers")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text("WebRTC ICE servers")
|
||||
}
|
||||
@@ -99,14 +134,24 @@ struct NetworkAndServers: View {
|
||||
currentNetCfg = getNetCfg()
|
||||
resetNetCfgView()
|
||||
}
|
||||
.onChange(of: onionHosts) { _ in
|
||||
if onionHosts != OnionHosts(netCfg: currentNetCfg) {
|
||||
alert = .updateOnionHosts(hosts: onionHosts)
|
||||
.onChange(of: onionHosts) { hosts in
|
||||
if hosts != OnionHosts(netCfg: currentNetCfg) {
|
||||
alert = .updateOnionHosts(hosts: hosts)
|
||||
}
|
||||
}
|
||||
.onChange(of: sessionMode) { _ in
|
||||
if sessionMode != netCfg.sessionMode {
|
||||
alert = .updateSessionMode(mode: sessionMode)
|
||||
.onChange(of: sessionMode) { mode in
|
||||
if mode != netCfg.sessionMode {
|
||||
alert = .updateSessionMode(mode: mode)
|
||||
}
|
||||
}
|
||||
.onChange(of: proxyMode) { mode in
|
||||
if mode != netCfg.smpProxyMode {
|
||||
alert = .updateSMPProxyMode(proxyMode: mode)
|
||||
}
|
||||
}
|
||||
.onChange(of: proxyFallback) { fallbackMode in
|
||||
if fallbackMode != netCfg.smpProxyFallback {
|
||||
alert = .updateSMPProxyFallback(proxyFallback: fallbackMode)
|
||||
}
|
||||
}
|
||||
.alert(item: $alert) { a in
|
||||
@@ -137,6 +182,30 @@ struct NetworkAndServers: View {
|
||||
resetNetCfgView()
|
||||
}
|
||||
)
|
||||
case let .updateSMPProxyMode(proxyMode):
|
||||
return Alert(
|
||||
title: Text("Message routing mode"),
|
||||
message: Text(proxyModeInfo(proxyMode)) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."),
|
||||
primaryButton: .default(Text("Ok")) {
|
||||
netCfg.smpProxyMode = proxyMode
|
||||
saveNetCfg()
|
||||
},
|
||||
secondaryButton: .cancel() {
|
||||
resetNetCfgView()
|
||||
}
|
||||
)
|
||||
case let .updateSMPProxyFallback(proxyFallback):
|
||||
return Alert(
|
||||
title: Text("Message routing fallback"),
|
||||
message: Text(proxyFallbackInfo(proxyFallback)) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."),
|
||||
primaryButton: .default(Text("Ok")) {
|
||||
netCfg.smpProxyFallback = proxyFallback
|
||||
saveNetCfg()
|
||||
},
|
||||
secondaryButton: .cancel() {
|
||||
resetNetCfgView()
|
||||
}
|
||||
)
|
||||
case let .error(err):
|
||||
return Alert(
|
||||
title: Text("Error updating settings"),
|
||||
@@ -166,6 +235,8 @@ struct NetworkAndServers: View {
|
||||
netCfg = currentNetCfg
|
||||
onionHosts = OnionHosts(netCfg: netCfg)
|
||||
sessionMode = netCfg.sessionMode
|
||||
proxyMode = netCfg.smpProxyMode
|
||||
proxyFallback = netCfg.smpProxyFallback
|
||||
}
|
||||
|
||||
private func onionHostsInfo(_ hosts: OnionHosts) -> LocalizedStringKey {
|
||||
@@ -182,6 +253,23 @@ struct NetworkAndServers: View {
|
||||
case .entity: return "A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail."
|
||||
}
|
||||
}
|
||||
|
||||
private func proxyModeInfo(_ mode: SMPProxyMode) -> LocalizedStringKey {
|
||||
switch mode {
|
||||
case .always: return "Always use private routing."
|
||||
case .unknown: return "Use private routing with unknown servers."
|
||||
case .unprotected: return "Use private routing with unknown servers when IP address is not protected."
|
||||
case .never: return "Do NOT use private routing."
|
||||
}
|
||||
}
|
||||
|
||||
private func proxyFallbackInfo(_ proxyFallback: SMPProxyFallback) -> LocalizedStringKey {
|
||||
switch proxyFallback {
|
||||
case .allow: return "Send messages directly when your or destination server does not support private routing."
|
||||
case .allowProtected: return "Send messages directly when IP address is protected and your or destination server does not support private routing."
|
||||
case .prohibit: return "Do NOT send messages directly, even if your or destination server does not support private routing."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NetworkServersView_Previews: PreviewProvider {
|
||||
|
||||
@@ -33,6 +33,7 @@ struct NotificationsView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Send notifications")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.alert(item: $showAlert) { alert in
|
||||
if let token = m.deviceToken {
|
||||
@@ -68,6 +69,7 @@ struct NotificationsView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Show preview")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} label: {
|
||||
HStack {
|
||||
@@ -172,6 +174,7 @@ 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)?
|
||||
@@ -185,11 +188,11 @@ struct SelectionListView<Item: SelectableItem>: View {
|
||||
if selection == item {
|
||||
Image(systemName: "checkmark")
|
||||
.resizable().scaledToFit().frame(width: 16)
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.listRowBackground(Color(uiColor: tapped == item ? .secondarySystemFill : .systemBackground))
|
||||
.listRowBackground(tapped == item ? Color(uiColor: .secondarySystemFill) : theme.colors.background)
|
||||
.onTapGesture {
|
||||
if selection == item { return }
|
||||
if let f = onSelection {
|
||||
|
||||
@@ -16,6 +16,7 @@ struct PrivacySettings: View {
|
||||
@AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true
|
||||
@AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
|
||||
@AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true
|
||||
@AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true
|
||||
@State private var simplexLinkMode = privacySimplexLinkModeDefault.get()
|
||||
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||
@@ -47,6 +48,7 @@ struct PrivacySettings: View {
|
||||
NavigationLink {
|
||||
SimplexLockView(prefPerformLA: $prefPerformLA, currentLAMode: $currentLAMode)
|
||||
.navigationTitle("SimpleX Lock")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
if prefPerformLA {
|
||||
settingsRow("lock.fill", color: .green) {
|
||||
@@ -64,18 +66,6 @@ struct PrivacySettings: View {
|
||||
}
|
||||
|
||||
Section {
|
||||
settingsRow("lock.doc") {
|
||||
Toggle("Encrypt local files", isOn: $encryptLocalFiles)
|
||||
.onChange(of: encryptLocalFiles) {
|
||||
setEncryptLocalFiles($0)
|
||||
}
|
||||
}
|
||||
settingsRow("photo") {
|
||||
Toggle("Auto-accept images", isOn: $autoAcceptImages)
|
||||
.onChange(of: autoAcceptImages) {
|
||||
privacyAcceptImagesGroupDefault.set($0)
|
||||
}
|
||||
}
|
||||
settingsRow("network") {
|
||||
Toggle("Send link previews", isOn: $useLinkPreviews)
|
||||
}
|
||||
@@ -108,6 +98,32 @@ struct PrivacySettings: View {
|
||||
Text("Chats")
|
||||
}
|
||||
|
||||
Section {
|
||||
settingsRow("lock.doc") {
|
||||
Toggle("Encrypt local files", isOn: $encryptLocalFiles)
|
||||
.onChange(of: encryptLocalFiles) {
|
||||
setEncryptLocalFiles($0)
|
||||
}
|
||||
}
|
||||
settingsRow("photo") {
|
||||
Toggle("Auto-accept images", isOn: $autoAcceptImages)
|
||||
.onChange(of: autoAcceptImages) {
|
||||
privacyAcceptImagesGroupDefault.set($0)
|
||||
}
|
||||
}
|
||||
settingsRow("network.badge.shield.half.filled") {
|
||||
Toggle("Protect IP address", isOn: $askToApproveRelays)
|
||||
}
|
||||
} header: {
|
||||
Text("Files")
|
||||
} footer: {
|
||||
if askToApproveRelays {
|
||||
Text("The app will ask to confirm downloads from unknown file servers (except .onion).")
|
||||
} else {
|
||||
Text("Without Tor or VPN, your IP address will be visible to file servers.")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
settingsRow("person") {
|
||||
Toggle("Contacts", isOn: $contactReceipts)
|
||||
@@ -317,6 +333,7 @@ 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
|
||||
@@ -401,7 +418,7 @@ struct SimplexLockView: View {
|
||||
HStack(spacing: 6) {
|
||||
Text("Enable self-destruct")
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.onTapGesture {
|
||||
|
||||
@@ -94,6 +94,7 @@ struct ProtocolServersView: View {
|
||||
}
|
||||
.sheet(isPresented: $showScanProtoServer) {
|
||||
ScanProtocolServer(servers: $servers)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
.modifier(BackButton(disabled: Binding.constant(false)) {
|
||||
if saveDisabled {
|
||||
@@ -171,6 +172,7 @@ struct ProtocolServersView: View {
|
||||
serverToEdit: srv
|
||||
)
|
||||
.navigationBarTitle(srv.preset ? "Preset server" : "Your server")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
let address = parseServerAddress(srv.server)
|
||||
|
||||
@@ -89,7 +89,7 @@ struct SetDeliveryReceiptsView: View {
|
||||
.padding()
|
||||
.padding(.horizontal)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color(uiColor: .systemBackground))
|
||||
.background(AppTheme.shared.colors.background)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ let DEFAULT_ENCRYPTION_STARTED_AT = "encryptionStartedAt"
|
||||
let DEFAULT_ACCENT_COLOR_RED = "accentColorRed"
|
||||
let DEFAULT_ACCENT_COLOR_GREEN = "accentColorGreen"
|
||||
let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue"
|
||||
let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle"
|
||||
//let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle"
|
||||
let DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius"
|
||||
let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab"
|
||||
let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown"
|
||||
@@ -60,6 +60,12 @@ let DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS = "deviceNameForRemoteAccess"
|
||||
let DEFAULT_CONFIRM_REMOTE_SESSIONS = "confirmRemoteSessions"
|
||||
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast"
|
||||
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto"
|
||||
let DEFAULT_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
|
||||
|
||||
let DEFAULT_CURRENT_THEME = "currentTheme"
|
||||
let DEFAULT_SYSTEM_DARK_THEME = "systemDarkTheme"
|
||||
let DEFAULT_CURRENT_THEME_IDS = "currentThemeIds"
|
||||
let DEFAULT_THEME_OVERRIDES = "themeOverrides"
|
||||
|
||||
let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen"
|
||||
|
||||
@@ -87,7 +93,7 @@ let appDefaults: [String: Any] = [
|
||||
DEFAULT_ACCENT_COLOR_RED: 0.000,
|
||||
DEFAULT_ACCENT_COLOR_GREEN: 0.533,
|
||||
DEFAULT_ACCENT_COLOR_BLUE: 1.000,
|
||||
DEFAULT_USER_INTERFACE_STYLE: 0,
|
||||
//DEFAULT_USER_INTERFACE_STYLE: 0,
|
||||
DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner,
|
||||
DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue,
|
||||
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false,
|
||||
@@ -99,7 +105,13 @@ let appDefaults: [String: Any] = [
|
||||
DEFAULT_CONFIRM_REMOTE_SESSIONS: false,
|
||||
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: true,
|
||||
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true,
|
||||
ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue
|
||||
DEFAULT_SHOW_SENT_VIA_RPOXY: false,
|
||||
ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue,
|
||||
|
||||
DEFAULT_THEME_OVERRIDES: "{}",
|
||||
DEFAULT_CURRENT_THEME: DefaultTheme.SYSTEM_THEME_NAME,
|
||||
DEFAULT_SYSTEM_DARK_THEME: DefaultTheme.SIMPLEX.themeName,
|
||||
DEFAULT_CURRENT_THEME_IDS: "{}"
|
||||
]
|
||||
|
||||
// not used anymore
|
||||
@@ -146,14 +158,66 @@ let onboardingStageDefault = EnumDefault<OnboardingStage>(defaults: UserDefaults
|
||||
|
||||
let customDisappearingMessageTimeDefault = IntDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME)
|
||||
|
||||
let currentThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME, withDefault: DefaultTheme.SYSTEM_THEME_NAME)
|
||||
let systemDarkThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SYSTEM_DARK_THEME, withDefault: DefaultTheme.SIMPLEX.themeName)
|
||||
let currentThemeIdsDefault = CodableDefault<[String: String]>(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME_IDS, withDefault: [:] )
|
||||
let themeOverridesDefault: CodableDefault<[ThemeOverrides]> = CodableDefault(defaults: UserDefaults.standard, forKey: DEFAULT_THEME_OVERRIDES, withDefault: [])
|
||||
|
||||
func setGroupDefaults() {
|
||||
privacyAcceptImagesGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES))
|
||||
}
|
||||
|
||||
public class StringDefault {
|
||||
var defaults: UserDefaults
|
||||
var key: String
|
||||
var defaultValue: String
|
||||
|
||||
public init(defaults: UserDefaults = UserDefaults.standard, forKey: String, withDefault: String) {
|
||||
self.defaults = defaults
|
||||
self.key = forKey
|
||||
self.defaultValue = withDefault
|
||||
}
|
||||
|
||||
public func get() -> String {
|
||||
defaults.string(forKey: key) ?? defaultValue
|
||||
}
|
||||
|
||||
public func set(_ value: String) {
|
||||
defaults.set(value, forKey: key)
|
||||
defaults.synchronize()
|
||||
}
|
||||
}
|
||||
|
||||
public class CodableDefault<T: Codable> {
|
||||
var defaults: UserDefaults
|
||||
var key: String
|
||||
var defaultValue: T
|
||||
|
||||
public init(defaults: UserDefaults = UserDefaults.standard, forKey: String, withDefault: T) {
|
||||
self.defaults = defaults
|
||||
self.key = forKey
|
||||
self.defaultValue = withDefault
|
||||
}
|
||||
|
||||
public func get() -> T {
|
||||
if let value = defaults.string(forKey: key) {
|
||||
return decodeJSON(value) ?? defaultValue
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
public func set(_ value: T) {
|
||||
defaults.set(encodeJSON(value), forKey: key)
|
||||
defaults.synchronize()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct SettingsView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var sceneDelegate: SceneDelegate
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var showSettings: Bool
|
||||
@State private var showProgress: Bool = false
|
||||
|
||||
@@ -178,6 +242,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
UserProfile()
|
||||
.navigationTitle("Your current profile")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
ProfilePreview(profileOf: user)
|
||||
.padding(.leading, -8)
|
||||
@@ -195,6 +260,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
UserAddressView(shareViaProfile: user.addressShared)
|
||||
.navigationTitle("SimpleX address")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
settingsRow("qrcode") { Text("Your SimpleX address") }
|
||||
@@ -203,6 +269,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
PreferencesView(profile: user.profile, preferences: user.fullPreferences, currentPreferences: user.fullPreferences)
|
||||
.navigationTitle("Your preferences")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
settingsRow("switch.2") { Text("Chat preferences") }
|
||||
}
|
||||
@@ -217,6 +284,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
MigrateFromDevice(showSettings: $showSettings, showProgressOnSettings: $showProgress)
|
||||
.navigationTitle("Migrate device")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
settingsRow("tray.and.arrow.up") { Text("Migrate to another device") }
|
||||
@@ -228,6 +296,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
NotificationsView()
|
||||
.navigationTitle("Notifications")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
HStack {
|
||||
notificationsIcon()
|
||||
@@ -239,6 +308,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
NetworkAndServers()
|
||||
.navigationTitle("Network & servers")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
settingsRow("externaldrive.connected.to.line.below") { Text("Network & servers") }
|
||||
}
|
||||
@@ -247,6 +317,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
CallSettings()
|
||||
.navigationTitle("Your calls")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
settingsRow("video") { Text("Audio & video calls") }
|
||||
}
|
||||
@@ -255,6 +326,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
PrivacySettings()
|
||||
.navigationTitle("Your privacy")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
settingsRow("lock") { Text("Privacy & security") }
|
||||
}
|
||||
@@ -264,6 +336,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
AppearanceSettings()
|
||||
.navigationTitle("Appearance")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
settingsRow("sun.max") { Text("Appearance") }
|
||||
}
|
||||
@@ -278,6 +351,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
ChatHelp(showSettings: $showSettings)
|
||||
.navigationTitle("Welcome \(user.displayName)!")
|
||||
.modifier(ThemedBackground())
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
} label: {
|
||||
settingsRow("questionmark") { Text("How to use it") }
|
||||
@@ -285,6 +359,7 @@ struct SettingsView: View {
|
||||
}
|
||||
NavigationLink {
|
||||
WhatsNewView(viaSettings: true)
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} label: {
|
||||
settingsRow("plus") { Text("What's new") }
|
||||
@@ -292,6 +367,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
SimpleXInfo(onboarding: false)
|
||||
.navigationBarTitle("", displayMode: .inline)
|
||||
.modifier(ThemedBackground())
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
} label: {
|
||||
settingsRow("info") { Text("About SimpleX Chat") }
|
||||
@@ -331,18 +407,21 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
DeveloperView()
|
||||
.navigationTitle("Developer tools")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
settingsRow("chevron.left.forwardslash.chevron.right") { Text("Developer tools") }
|
||||
}
|
||||
NavigationLink {
|
||||
VersionView()
|
||||
.navigationBarTitle("App version")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Your settings")
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
.onDisappear {
|
||||
chatModel.showingTerminal = false
|
||||
@@ -354,6 +433,7 @@ struct SettingsView: View {
|
||||
NavigationLink {
|
||||
DatabaseView(showSettings: $showSettings, chatItemTTL: chatModel.chatItemTTL)
|
||||
.navigationTitle("Your chat database")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
let color: Color = chatModel.chatDbEncrypted == false ? .orange : .secondary
|
||||
settingsRow("internaldrive", color: color) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user