Compare commits

..

33 Commits

Author SHA1 Message Date
Evgeny Poberezkin 0d62d7ddfb Merge branch 'master' into ab/diff-subs 2024-05-28 08:06:25 +01:00
Stanislav Dmitrenko b58ccedf23 android, desktop: enhancements to wallpapers (#4234)
* android, desktop: enhancements to wallpapers

* fixes

* unneeded
2024-05-28 08:02:45 +01:00
Evgeny Poberezkin a78aeb47c5 Merge branch 'master' into ab/diff-subs 2024-05-27 16:00:55 +01:00
spaced4ndy 01cadefde7 core: avoid creating duplicate introductions between group members connecting concurrently (#4235) 2024-05-27 18:42:39 +04:00
Evgeny Poberezkin c70e7223d9 core: delete connection records when connecting (JOIN) fails (#4233)
* core: delete connection records when connecting (JOIN) fails

* show errors in commands
2024-05-27 15:32:09 +01:00
Evgeny Poberezkin a1d4f52185 5.8-beta.3: ios 220, android 214, desktop 49 2024-05-25 20:57:07 +01:00
Evgeny Poberezkin 373070f388 ui: show UI for additional group preferences (SimpleX links and per-role permissions) 2024-05-25 19:54:05 +01:00
Evgeny Poberezkin a03ccd0ba6 ios: update core library 5.8.0.3 2024-05-25 18:55:15 +01:00
Evgeny Poberezkin a7982cccf9 Merge branch 'stable' 2024-05-25 17:22:25 +01:00
Evgeny Poberezkin dccd6631af 5.7.4: ios 219, android 212, desktop 48 2024-05-25 17:06:11 +01:00
Evgeny Poberezkin b0516b6440 core: 5.7.4.1 2024-05-25 11:18:49 +01:00
Stanislav Dmitrenko bf91666f07 android, desktop: wallpapers (#4109)
* android, desktop: wallpapers

* changes

* removed unused

* changes

* backgrounds moved to themes. can be exported/imported

* changes and refactoring

* scrollable

* change

* reorder

* theme per background

* changes

* don't drop customization in same cases

* changes

* changes

* fix state update

* different defaults

* fix Android

* removed migration

* temporary

* returned theme name as uppercase back

* resilient to wrong preset name

* more

* live color change without button

* fix

* Revert "temporary"

This reverts commit 705c92ee56.

* changes

* changes

* almost candy

* old picker and ability to specify hex color

* different image

* picker

* user theme override, fixes, import/export theme with image

* revert when about to merge: useful layout for theming

* custom image displaying and deletion work correctly

* performance

* moved code up

* changes

* color changes in picker when changing themes

* reset color without closing modal

* unused

* log

* currentTheme instead of MaterialTheme

* different inheritance logic

* fix and move sent/received message colors to top

* remove background

* performance

* fix wrong base

* changes

* fix

* small

* custom image selection made normally

* better color picker

* better picker

* refactor, fixes, optimizations

* working with unknown base theme + tests

* change

* delete wallpaper dir when importing archive

* function rename

* fix bug with state update

* update colors

* fix text colors

* update images

* changes

* remote desktop and theme mode dropdown

* default theme

* additional accent 2 changes

* fix default wallpaper

* removed ChatListAlwaysVisible

* renaming

* update messages

* changes

* default theme

* unused

* member icon background color

* image quality

* fix wallpaper on empty notes screen

* update colors

* change

* update colors

* fix update on android

* fix update on both platforms

* update colors, remove serializable

* fix color

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-05-25 09:52:16 +01:00
Evgeny Poberezkin ab47a5a27e Revert "ui: comment smp proxy ui (#4204)"
This reverts commit a0d6ae15ab.
2024-05-24 22:59:37 +01:00
Evgeny Poberezkin 23581f59ae core: 5.8.0.3 2024-05-24 21:33:45 +01:00
Evgeny Poberezkin 291df6e9d0 core: logging of chat events (#4216)
* core: update simplexmq (persist server errors)

* fix

* same config

* logging

* logging 2

* log

* log 2

* finally

* catch better

* more logs

* logs

* fix

* more logging, context from PROHIBITED

* warning

* more logs

* logs3

* logs4

* logs in simplexmq

* log locks from simplemq

* log queue size

* log sendMessagesB in simplexmq

* update simplexmq

* logs5

* logs6

* logs7

* logs8

* logs8

* logs9

* logs10

* log11

* log12

* fix test

* more logs

* logging

* clean up

* refactor

* simplify

* tags

* log level

* remove network errors from the log

* rename
2024-05-24 21:09:21 +01:00
Evgeny Poberezkin dae0b63c22 Merge branch 'stable' 2024-05-23 22:56:04 +01:00
Evgeny Poberezkin 0964982782 core: ensure that messages are sent to different members (#4221)
* core: ensure that messages are sent to different members

* update simplexmq
2024-05-23 22:50:23 +01:00
Evgeny Poberezkin 9eb6034e1a core: 5.7.4.0 (simplexmq 5.7.4.1: networking improvements) 2024-05-22 20:30:24 +01:00
IC Rainbow 94df5d47d1 Merge remote-tracking branch 'origin/master' into ab/diff-subs 2024-05-20 13:11:55 +03:00
Evgeny Poberezkin f9cba3f8e0 Merge branch 'master' into ab/diff-subs 2024-05-16 19:11:49 +01:00
Evgeny Poberezkin 3655971aec Merge branch 'master' into ab/diff-subs 2024-05-15 13:57:39 +01:00
Alexander Bondarenko dea45e3228 Merge remote-tracking branch 'origin/master' into ab/diff-subs 2024-05-15 14:58:08 +03:00
Alexander Bondarenko 4f2102ac6e use user and diff filters 2024-05-14 16:56:28 +03:00
IC Rainbow 17bcdaf6c4 Merge remote-tracking branch 'origin/master' into ab/diff-subs 2024-05-14 13:41:38 +03:00
IC Rainbow 1a2fad070e Merge remote-tracking branch 'origin/master' into ab/diff-subs 2024-05-13 15:56:29 +03:00
IC Rainbow 6101be5657 update mq 2024-05-12 21:55:35 +03:00
Evgeny Poberezkin a10dd81a1d Merge branch 'master' into ab/diff-subs 2024-05-12 12:07:26 +01:00
Evgeny Poberezkin 289130832a Merge branch 'master' into ab/diff-subs 2024-05-11 23:59:15 +01:00
Evgeny Poberezkin 224cfe9e02 Merge branch 'master' into ab/diff-subs 2024-05-11 13:07:05 +01:00
Alexander Bondarenko 0679d023e2 Merge remote-tracking branch 'origin/master' into ab/diff-subs 2024-05-10 21:01:33 +03:00
Alexander Bondarenko 6d5869e974 Merge remote-tracking branch 'origin/master' into ab/diff-subs 2024-05-08 16:42:54 +03:00
Alexander Bondarenko f0974af03a sync 2024-05-08 16:13:42 +03:00
Alexander Bondarenko da70aa30b6 debug: use client diffs in /get subs 2024-05-08 16:09:05 +03:00
116 changed files with 3899 additions and 1090 deletions
@@ -34,8 +34,7 @@ struct GroupPreferencesView: View {
featureSection(.reactions, $preferences.reactions.enable) featureSection(.reactions, $preferences.reactions.enable)
featureSection(.voice, $preferences.voice.enable, $preferences.voice.role) featureSection(.voice, $preferences.voice.enable, $preferences.voice.role)
featureSection(.files, $preferences.files.enable, $preferences.files.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) featureSection(.history, $preferences.history.enable)
if groupInfo.canEdit { if groupInfo.canEdit {
@@ -102,8 +101,6 @@ struct GroupPreferencesView: View {
} }
} }
.frame(height: 36) .frame(height: 36)
// remove in v5.8
.disabled(true)
} }
} else { } else {
settingsRow(icon, color: color) { settingsRow(icon, color: color) {
+26 -26
View File
@@ -76,11 +76,6 @@
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */; }; 5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */; };
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; }; 5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; };
5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */; }; 5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */; };
5C9F3DD62BFBCDD90003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD12BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a */; };
5C9F3DD72BFBCDD90003B86B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD22BFBCDD80003B86B /* libffi.a */; };
5C9F3DD82BFBCDD90003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD32BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a */; };
5C9F3DD92BFBCDD90003B86B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD42BFBCDD90003B86B /* libgmpxx.a */; };
5C9F3DDA2BFBCDD90003B86B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD52BFBCDD90003B86B /* libgmp.a */; };
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; }; 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; };
5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; }; 5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; };
5CA059DE279559F40002BEB4 /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */; }; 5CA059DE279559F40002BEB4 /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */; };
@@ -144,6 +139,11 @@
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; }; 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; };
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; }; 5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; };
5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */; }; 5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */; };
5CEE87942C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE878F2C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a */; };
5CEE87952C024F4F00583B8A /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE87902C024F4F00583B8A /* libgmp.a */; };
5CEE87962C024F4F00583B8A /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE87912C024F4F00583B8A /* libgmpxx.a */; };
5CEE87972C024F4F00583B8A /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE87922C024F4F00583B8A /* libffi.a */; };
5CEE87982C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE87932C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a */; };
5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */; }; 5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */; };
5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF937212B25034A00E1D781 /* NSESubscriber.swift */; }; 5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF937212B25034A00E1D781 /* NSESubscriber.swift */; };
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; }; 5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; };
@@ -354,11 +354,6 @@
5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = "<group>"; }; 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = "<group>"; };
5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = "<group>"; }; 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = "<group>"; };
5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFile.swift; sourceTree = "<group>"; }; 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFile.swift; sourceTree = "<group>"; };
5C9F3DD12BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a"; sourceTree = "<group>"; };
5C9F3DD22BFBCDD80003B86B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C9F3DD32BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a"; sourceTree = "<group>"; };
5C9F3DD42BFBCDD90003B86B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C9F3DD52BFBCDD90003B86B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; }; 5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = "<group>"; }; 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = "<group>"; };
5CA059C3279559F40002BEB4 /* SimpleXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXApp.swift; sourceTree = "<group>"; }; 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXApp.swift; sourceTree = "<group>"; };
@@ -440,6 +435,11 @@
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; }; 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; }; 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; };
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = "<group>"; }; 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = "<group>"; };
5CEE878F2C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a"; sourceTree = "<group>"; };
5CEE87902C024F4F00583B8A /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5CEE87912C024F4F00583B8A /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5CEE87922C024F4F00583B8A /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CEE87932C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a"; sourceTree = "<group>"; };
5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFileSubscriber.swift; sourceTree = "<group>"; }; 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFileSubscriber.swift; sourceTree = "<group>"; };
5CF937212B25034A00E1D781 /* NSESubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSESubscriber.swift; sourceTree = "<group>"; }; 5CF937212B25034A00E1D781 /* NSESubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSESubscriber.swift; sourceTree = "<group>"; };
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; }; 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; };
@@ -529,13 +529,13 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5C9F3DD92BFBCDD90003B86B /* libgmpxx.a in Frameworks */, 5CEE87942C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a in Frameworks */,
5C9F3DDA2BFBCDD90003B86B /* libgmp.a in Frameworks */, 5CEE87972C024F4F00583B8A /* libffi.a in Frameworks */,
5C9F3DD82BFBCDD90003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a in Frameworks */, 5CEE87962C024F4F00583B8A /* libgmpxx.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5C9F3DD62BFBCDD90003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a in Frameworks */, 5CEE87952C024F4F00583B8A /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5C9F3DD72BFBCDD90003B86B /* libffi.a in Frameworks */, 5CEE87982C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -601,11 +601,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = { 5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5C9F3DD22BFBCDD80003B86B /* libffi.a */, 5CEE87922C024F4F00583B8A /* libffi.a */,
5C9F3DD52BFBCDD90003B86B /* libgmp.a */, 5CEE87902C024F4F00583B8A /* libgmp.a */,
5C9F3DD42BFBCDD90003B86B /* libgmpxx.a */, 5CEE87912C024F4F00583B8A /* libgmpxx.a */,
5C9F3DD12BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a */, 5CEE878F2C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a */,
5C9F3DD32BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a */, 5CEE87932C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a */,
); );
path = Libraries; path = Libraries;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1552,7 +1552,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 218; CURRENT_PROJECT_VERSION = 220;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -1601,7 +1601,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 218; CURRENT_PROJECT_VERSION = 220;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -1687,7 +1687,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 218; CURRENT_PROJECT_VERSION = 220;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s; GCC_OPTIMIZATION_LEVEL = s;
@@ -1724,7 +1724,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 218; CURRENT_PROJECT_VERSION = 220;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO; ENABLE_CODE_COVERAGE = NO;
@@ -1761,7 +1761,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 218; CURRENT_PROJECT_VERSION = 220;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1812,7 +1812,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 218; CURRENT_PROJECT_VERSION = 220;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@@ -7,7 +7,7 @@ import chat.simplex.app.SimplexService.Companion.showPassphraseNotification
import chat.simplex.common.model.ChatController import chat.simplex.common.model.ChatController
import chat.simplex.common.views.helpers.DBMigrationResult import chat.simplex.common.views.helpers.DBMigrationResult
import chat.simplex.common.platform.chatModel import chat.simplex.common.platform.chatModel
import chat.simplex.common.platform.initChatControllerAndRunMigrations import chat.simplex.common.platform.initChatControllerOnStart
import chat.simplex.common.views.helpers.DatabaseUtils import chat.simplex.common.views.helpers.DatabaseUtils
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.util.Date import java.util.Date
@@ -60,7 +60,7 @@ class MessagesFetcherWork(
try { try {
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here // In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) { if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
initChatControllerAndRunMigrations() initChatControllerOnStart()
} }
withTimeout(durationSeconds * 1000L) { withTimeout(durationSeconds * 1000L) {
val chatController = ChatController val chatController = ChatController
@@ -6,7 +6,6 @@ import android.content.Context
import chat.simplex.common.platform.Log import chat.simplex.common.platform.Log
import android.content.Intent import android.content.Intent
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.media.AudioManager
import android.os.* import android.os.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@@ -66,6 +65,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
context = this context = this
initHaskell() initHaskell()
initMultiplatform() initMultiplatform()
runMigrations()
tmpDir.deleteRecursively() tmpDir.deleteRecursively()
tmpDir.mkdir() tmpDir.mkdir()
@@ -74,7 +74,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
// It's important, otherwise, user may be locked in undefined state // It's important, otherwise, user may be locked in undefined state
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
} else if (DatabaseUtils.ksAppPassword.get() == null || DatabaseUtils.ksSelfDestructPassword.get() == null) { } else if (DatabaseUtils.ksAppPassword.get() == null || DatabaseUtils.ksSelfDestructPassword.get() == null) {
initChatControllerAndRunMigrations() initChatControllerOnStart()
} }
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp) ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
} }
@@ -254,7 +254,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
override fun androidSetNightModeIfSupported() { override fun androidSetNightModeIfSupported() {
if (Build.VERSION.SDK_INT < 31) return if (Build.VERSION.SDK_INT < 31) return
val light = if (CurrentColors.value.name == DefaultTheme.SYSTEM.name) { val light = if (CurrentColors.value.name == DefaultTheme.SYSTEM_THEME_NAME) {
null null
} else { } else {
CurrentColors.value.colors.isLight CurrentColors.value.colors.isLight
@@ -77,7 +77,7 @@ class SimplexService: Service() {
isServiceStarted = true isServiceStarted = true
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here // In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) { if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
initChatControllerAndRunMigrations() initChatControllerOnStart()
} }
} }
} }
+4 -1
View File
@@ -37,7 +37,7 @@ kotlin {
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
api("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0") api("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0")
api("com.russhwolf:multiplatform-settings:1.1.1") api("com.russhwolf:multiplatform-settings:1.1.1")
api("com.charleskorn.kaml:kaml:0.58.0") api("com.charleskorn.kaml:kaml:0.59.0")
api("org.jetbrains.compose.ui:ui-text:${rootProject.extra["compose.version"] as String}") api("org.jetbrains.compose.ui:ui-text:${rootProject.extra["compose.version"] as String}")
implementation("org.jetbrains.compose.components:components-animatedimage:${rootProject.extra["compose.version"] as String}") implementation("org.jetbrains.compose.components:components-animatedimage:${rootProject.extra["compose.version"] as String}")
//Barcode //Barcode
@@ -53,6 +53,9 @@ kotlin {
val commonTest by getting { val commonTest by getting {
dependencies { dependencies {
implementation(kotlin("test")) implementation(kotlin("test"))
implementation(kotlin("test-junit"))
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
} }
} }
val androidMain by getting { val androidMain by getting {
@@ -15,8 +15,10 @@ actual val dataDir: File = androidAppContext.dataDir
actual val tmpDir: File = androidAppContext.getDir("temp", Application.MODE_PRIVATE) actual val tmpDir: File = androidAppContext.getDir("temp", Application.MODE_PRIVATE)
actual val filesDir: File = File(dataDir.absolutePath + File.separator + "files") actual val filesDir: File = File(dataDir.absolutePath + File.separator + "files")
actual val appFilesDir: File = File(filesDir.absolutePath + File.separator + "app_files") actual val appFilesDir: File = File(filesDir.absolutePath + File.separator + "app_files")
actual val wallpapersDir: File = File(filesDir.absolutePath + File.separator + "assets" + File.separator + "wallpapers").also { it.mkdirs() }
actual val coreTmpDir: File = File(filesDir.absolutePath + File.separator + "temp_files") actual val coreTmpDir: File = File(filesDir.absolutePath + File.separator + "temp_files")
actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "files" actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "files"
actual val preferencesDir = File(dataDir.absolutePath + File.separator + "shared_prefs")
actual val chatDatabaseFileName: String = "files_chat.db" actual val chatDatabaseFileName: String = "files_chat.db"
actual val agentDatabaseFileName: String = "files_agent.db" actual val agentDatabaseFileName: String = "files_agent.db"
@@ -7,6 +7,8 @@ import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.text.BidiFormatter import android.text.BidiFormatter
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.Font
@@ -14,9 +16,11 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
import chat.simplex.common.model.AppPreferences import chat.simplex.common.model.AppPreferences
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings
import com.russhwolf.settings.SharedPreferencesSettings import com.russhwolf.settings.SharedPreferencesSettings
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.desc.desc import dev.icerock.moko.resources.desc.desc
@@ -51,3 +55,6 @@ actual fun windowWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp
actual fun desktopExpandWindowToWidth(width: Dp) {} actual fun desktopExpandWindowToWidth(width: Dp) {}
actual fun isRtl(text: CharSequence): Boolean = BidiFormatter.getInstance().isRtl(text) actual fun isRtl(text: CharSequence): Boolean = BidiFormatter.getInstance().isRtl(text)
actual fun ImageResource.toComposeImageBitmap(): ImageBitmap? =
getDrawable(androidAppContext)?.toBitmap()?.asImageBitmap()
@@ -19,14 +19,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.*
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import chat.simplex.common.R
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
@@ -34,7 +30,6 @@ import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.helpers.APPLICATION_ID import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.common.helpers.saveAppLocale import chat.simplex.common.helpers.saveAppLocale
import chat.simplex.common.views.usersettings.AppearanceScope.ColorEditor
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
@@ -46,9 +41,8 @@ enum class AppIcon(val image: ImageResource) {
} }
@Composable @Composable
actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) { actual fun AppearanceView(m: ChatModel) {
val appIcon = remember { mutableStateOf(findEnabledIcon()) } val appIcon = remember { mutableStateOf(findEnabledIcon()) }
fun setAppIcon(newIcon: AppIcon) { fun setAppIcon(newIcon: AppIcon) {
if (appIcon.value == newIcon) return if (appIcon.value == newIcon) return
val newComponent = ComponentName(APPLICATION_ID, "chat.simplex.app.MainActivity_${newIcon.name.lowercase()}") val newComponent = ComponentName(APPLICATION_ID, "chat.simplex.app.MainActivity_${newIcon.name.lowercase()}")
@@ -65,18 +59,11 @@ actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatMod
appIcon.value = newIcon appIcon.value = newIcon
} }
AppearanceScope.AppearanceLayout( AppearanceScope.AppearanceLayout(
appIcon, appIcon,
m.controller.appPrefs.appLanguage, m.controller.appPrefs.appLanguage,
m.controller.appPrefs.systemDarkTheme, m.controller.appPrefs.systemDarkTheme,
changeIcon = ::setAppIcon, changeIcon = ::setAppIcon,
showSettingsModal = showSettingsModal,
editColor = { name, initialColor ->
ModalManager.start.showModalCloseable { close ->
ColorEditor(name, initialColor, close)
}
},
) )
} }
@@ -86,8 +73,6 @@ fun AppearanceScope.AppearanceLayout(
languagePref: SharedPreference<String?>, languagePref: SharedPreference<String?>,
systemDarkTheme: SharedPreference<String?>, systemDarkTheme: SharedPreference<String?>,
changeIcon: (AppIcon) -> Unit, changeIcon: (AppIcon) -> Unit,
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
editColor: (ThemeColor, Color) -> Unit,
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
@@ -120,6 +105,13 @@ fun AppearanceScope.AppearanceLayout(
} }
// } // }
} }
SectionDividerSpaced(maxTopPadding = true)
ThemesSection(systemDarkTheme)
SectionDividerSpaced(maxTopPadding = true)
ProfileImageSection()
SectionDividerSpaced() SectionDividerSpaced()
SectionView(stringResource(MR.strings.settings_section_title_icon), padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) { SectionView(stringResource(MR.strings.settings_section_title_icon), padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
@@ -145,11 +137,6 @@ fun AppearanceScope.AppearanceLayout(
} }
} }
SectionDividerSpaced(maxTopPadding = true)
ProfileImageSection()
SectionDividerSpaced(maxTopPadding = true)
ThemesSection(systemDarkTheme, showSettingsModal, editColor)
SectionBottomSpacer() SectionBottomSpacer()
} }
} }
@@ -169,8 +156,6 @@ fun PreviewAppearanceSettings() {
languagePref = SharedPreference({ null }, {}), languagePref = SharedPreference({ null }, {}),
systemDarkTheme = SharedPreference({ null }, {}), systemDarkTheme = SharedPreference({ null }, {}),
changeIcon = {}, changeIcon = {},
showSettingsModal = { {} },
editColor = { _, _ -> },
) )
} }
} }
@@ -4,7 +4,7 @@ import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.*
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.* import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
@@ -399,6 +399,18 @@ object ChatModel {
currentUser.value = updated currentUser.value = updated
} }
fun updateCurrentUserUiThemes(rhId: Long?, uiThemes: ThemeModeOverrides?) {
val current = currentUser.value ?: return
val updated = current.copy(
uiThemes = uiThemes
)
val i = users.indexOfFirst { it.user.userId == current.userId && it.user.remoteHostId == rhId }
if (i != -1) {
users[i] = users[i].copy(user = updated)
}
currentUser.value = updated
}
suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem { suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem {
val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct) val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -682,7 +694,8 @@ data class User(
override val showNtfs: Boolean, override val showNtfs: Boolean,
val sendRcptsContacts: Boolean, val sendRcptsContacts: Boolean,
val sendRcptsSmallGroups: Boolean, val sendRcptsSmallGroups: Boolean,
val viewPwdHash: UserPwdHash? val viewPwdHash: UserPwdHash?,
val uiThemes: ThemeModeOverrides? = null,
): NamedChat, UserLike { ): NamedChat, UserLike {
override val displayName: String get() = profile.displayName override val displayName: String get() = profile.displayName
override val fullName: String get() = profile.fullName override val fullName: String get() = profile.fullName
@@ -709,6 +722,7 @@ data class User(
sendRcptsContacts = true, sendRcptsContacts = true,
sendRcptsSmallGroups = false, sendRcptsSmallGroups = false,
viewPwdHash = null, viewPwdHash = null,
uiThemes = null,
) )
} }
} }
@@ -1041,7 +1055,8 @@ data class Contact(
override val updatedAt: Instant, override val updatedAt: Instant,
val chatTs: Instant?, val chatTs: Instant?,
val contactGroupMemberId: Long? = null, val contactGroupMemberId: Long? = null,
val contactGrpInvSent: Boolean val contactGrpInvSent: Boolean,
val uiThemes: ThemeModeOverrides? = null,
): SomeChat, NamedChat { ): SomeChat, NamedChat {
override val chatType get() = ChatType.Direct override val chatType get() = ChatType.Direct
override val id get() = "@$contactId" override val id get() = "@$contactId"
@@ -1113,7 +1128,8 @@ data class Contact(
createdAt = Clock.System.now(), createdAt = Clock.System.now(),
updatedAt = Clock.System.now(), updatedAt = Clock.System.now(),
chatTs = Clock.System.now(), chatTs = Clock.System.now(),
contactGrpInvSent = false contactGrpInvSent = false,
uiThemes = null,
) )
} }
} }
@@ -1253,7 +1269,8 @@ data class GroupInfo (
val chatSettings: ChatSettings, val chatSettings: ChatSettings,
override val createdAt: Instant, override val createdAt: Instant,
override val updatedAt: Instant, override val updatedAt: Instant,
val chatTs: Instant? val chatTs: Instant?,
val uiThemes: ThemeModeOverrides? = null,
): SomeChat, NamedChat { ): SomeChat, NamedChat {
override val chatType get() = ChatType.Group override val chatType get() = ChatType.Group
override val id get() = "#$groupId" override val id get() = "#$groupId"
@@ -1295,7 +1312,8 @@ data class GroupInfo (
chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false), chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false),
createdAt = Clock.System.now(), createdAt = Clock.System.now(),
updatedAt = Clock.System.now(), updatedAt = Clock.System.now(),
chatTs = Clock.System.now() chatTs = Clock.System.now(),
uiThemes = null,
) )
} }
} }
@@ -1915,12 +1933,13 @@ data class ChatItem (
itemDeleted: CIDeleted? = null, itemDeleted: CIDeleted? = null,
itemEdited: Boolean = false, itemEdited: Boolean = false,
itemTimed: CITimed? = null, itemTimed: CITimed? = null,
itemLive: Boolean = false,
deletable: Boolean = true, deletable: Boolean = true,
editable: Boolean = true editable: Boolean = true
) = ) =
ChatItem( ChatItem(
chatDir = dir, chatDir = dir,
meta = CIMeta.getSample(id, ts, text, status, sentViaProxy, itemForwarded, itemDeleted, itemEdited, itemTimed, deletable, editable), meta = CIMeta.getSample(id, ts, text, status, sentViaProxy, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, deletable, editable),
content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)), content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)),
quotedItem = quotedItem, quotedItem = quotedItem,
reactions = listOf(), reactions = listOf(),
@@ -26,8 +26,7 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.* import kotlinx.serialization.*
import kotlinx.serialization.builtins.MapSerializer import kotlinx.serialization.builtins.*
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import java.util.Date import java.util.Date
@@ -168,13 +167,20 @@ class AppPreferences {
val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false) val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false)
val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null) val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null)
val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name) val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM_THEME_NAME)
val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.name) val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.themeName)
val themeOverrides = mkMapPreference(SHARED_PREFS_THEMES, mapOf(), encode = { val currentThemeIds = mkMapPreference(SHARED_PREFS_CURRENT_THEME_IDs, mapOf(), encode = {
json.encodeToString(MapSerializer(String.serializer(), String.serializer()), it)
}, decode = {
json.decodeFromString(MapSerializer(String.serializer(), String.serializer()), it)
})
// Deprecated. Remove key from preferences in 2025
val themeOverridesOld = mkMapPreference(SHARED_PREFS_THEMES_OLD, mapOf(), encode = {
json.encodeToString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it) json.encodeToString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it)
}, decode = { }, decode = {
json.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it) jsonCoerceInputValues.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it)
}, settingsThemes) }, settingsThemes)
val themeOverrides = mkThemeOverridesPreference()
val profileImageCornerRadius = mkFloatPreference(SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS, 22.5f) val profileImageCornerRadius = mkFloatPreference(SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS, 22.5f)
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null) val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
@@ -269,6 +275,12 @@ class AppPreferences {
set = fun(value) = prefs.putString(prefName, encode(value)) set = fun(value) = prefs.putString(prefName, encode(value))
) )
private fun mkThemeOverridesPreference(): SharedPreference<List<ThemeOverrides>> =
SharedPreference(
get = fun() = themeOverridesStore ?: (readThemeOverrides()).also { themeOverridesStore = it },
set = fun(value) { if (writeThemeOverrides(value)) { themeOverridesStore = value } }
)
companion object { companion object {
const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS" const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS"
internal const val SHARED_PREFS_THEMES_ID = "chat.simplex.app.THEMES" internal const val SHARED_PREFS_THEMES_ID = "chat.simplex.app.THEMES"
@@ -345,8 +357,10 @@ class AppPreferences {
private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName" private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName"
private const val SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED = "PQExperimentalEnabled" // no longer used private const val SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED = "PQExperimentalEnabled" // no longer used
private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme" private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme"
private const val SHARED_PREFS_CURRENT_THEME_IDs = "CurrentThemeIds"
private const val SHARED_PREFS_SYSTEM_DARK_THEME = "SystemDarkTheme" private const val SHARED_PREFS_SYSTEM_DARK_THEME = "SystemDarkTheme"
private const val SHARED_PREFS_THEMES = "Themes" private const val SHARED_PREFS_THEMES_OLD = "Themes"
private const val SHARED_PREFS_THEME_OVERRIDES = "ThemeOverrides"
private const val SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS = "ProfileImageCornerRadius" private const val SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS = "ProfileImageCornerRadius"
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion" private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode" private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode"
@@ -361,6 +375,8 @@ class AppPreferences {
private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled" private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled"
private const val SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS = "iOSCallKitCallsInRecents" private const val SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS = "iOSCallKitCallsInRecents"
private var themeOverridesStore: List<ThemeOverrides>? = null
} }
} }
@@ -445,8 +461,13 @@ object ChatController {
Log.d(TAG, "startChatWithTemporaryDatabase") Log.d(TAG, "startChatWithTemporaryDatabase")
val migrationActiveUser = apiGetActiveUser(null, ctrl) ?: apiCreateActiveUser(null, Profile(displayName = "Temp", fullName = ""), ctrl = ctrl) val migrationActiveUser = apiGetActiveUser(null, ctrl) ?: apiCreateActiveUser(null, Profile(displayName = "Temp", fullName = ""), ctrl = ctrl)
apiSetNetworkConfig(netCfg, ctrl) apiSetNetworkConfig(netCfg, ctrl)
apiSetTempFolder(getMigrationTempFilesDirectory().absolutePath, ctrl) apiSetAppFilePaths(
apiSetFilesFolder(getMigrationTempFilesDirectory().absolutePath, ctrl) getMigrationTempFilesDirectory().absolutePath,
getMigrationTempFilesDirectory().absolutePath,
wallpapersDir.parentFile.absolutePath,
remoteHostsDir.absolutePath,
ctrl
)
apiStartChat(ctrl) apiStartChat(ctrl)
return migrationActiveUser return migrationActiveUser
} }
@@ -665,22 +686,10 @@ object ChatController {
} }
} }
suspend fun apiSetTempFolder(tempFolder: String, ctrl: ChatCtrl? = null) { suspend fun apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String, remoteHostsFolder: String, ctrl: ChatCtrl? = null) {
val r = sendCmd(null, CC.SetTempFolder(tempFolder), ctrl) val r = sendCmd(null, CC.ApiSetAppFilePaths(filesFolder, tempFolder, assetsFolder, remoteHostsFolder), ctrl)
if (r is CR.CmdOk) return if (r is CR.CmdOk) return
throw Exception("failed to set temp folder: ${r.responseType} ${r.details}") throw Exception("failed to set app file paths: ${r.responseType} ${r.details}")
}
suspend fun apiSetFilesFolder(filesFolder: String, ctrl: ChatCtrl? = null) {
val r = sendCmd(null, CC.SetFilesFolder(filesFolder), ctrl)
if (r is CR.CmdOk) return
throw Exception("failed to set files folder: ${r.responseType} ${r.details}")
}
suspend fun apiSetRemoteHostsFolder(remoteHostsFolder: String) {
val r = sendCmd(null, CC.SetRemoteHostsFolder(remoteHostsFolder))
if (r is CR.CmdOk) return
throw Exception("failed to set remote hosts folder: ${r.responseType} ${r.details}")
} }
suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetEncryptLocalFiles(enable)) suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetEncryptLocalFiles(enable))
@@ -1166,6 +1175,20 @@ object ChatController {
return null return null
} }
suspend fun apiSetUserUIThemes(rh: Long?, userId: Long, themes: ThemeModeOverrides?): Boolean {
val r = sendCmd(rh, CC.ApiSetUserUIThemes(userId, themes))
if (r is CR.CmdOk) return true
Log.e(TAG, "apiSetUserUIThemes bad response: ${r.responseType} ${r.details}")
return false
}
suspend fun apiSetChatUIThemes(rh: Long?, chatId: ChatId, themes: ThemeModeOverrides?): Boolean {
val r = sendCmd(rh, CC.ApiSetChatUIThemes(chatId, themes))
if (r is CR.CmdOk) return true
Log.e(TAG, "apiSetChatUIThemes bad response: ${r.responseType} ${r.details}")
return false
}
suspend fun apiCreateUserAddress(rh: Long?): String? { suspend fun apiCreateUserAddress(rh: Long?): String? {
val userId = kotlin.runCatching { currentUserId("apiCreateUserAddress") }.getOrElse { return null } val userId = kotlin.runCatching { currentUserId("apiCreateUserAddress") }.getOrElse { return null }
val r = sendCmd(rh, CC.ApiCreateMyAddress(userId)) val r = sendCmd(rh, CC.ApiCreateMyAddress(userId))
@@ -2437,9 +2460,8 @@ sealed class CC {
class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC() class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC()
class StartChat(val mainApp: Boolean): CC() class StartChat(val mainApp: Boolean): CC()
class ApiStopChat: CC() class ApiStopChat: CC()
class SetTempFolder(val tempFolder: String): CC() @Serializable
class SetFilesFolder(val filesFolder: String): CC() class ApiSetAppFilePaths(val appFilesFolder: String, val appTempFolder: String, val appAssetsFolder: String, val appRemoteHostsFolder: String): CC()
class SetRemoteHostsFolder(val remoteHostsFolder: String): CC()
class ApiSetEncryptLocalFiles(val enable: Boolean): CC() class ApiSetEncryptLocalFiles(val enable: Boolean): CC()
class ApiExportArchive(val config: ArchiveConfig): CC() class ApiExportArchive(val config: ArchiveConfig): CC()
class ApiImportArchive(val config: ArchiveConfig): CC() class ApiImportArchive(val config: ArchiveConfig): CC()
@@ -2507,6 +2529,8 @@ sealed class CC {
class ApiSetContactPrefs(val contactId: Long, val prefs: ChatPreferences): CC() class ApiSetContactPrefs(val contactId: Long, val prefs: ChatPreferences): CC()
class ApiSetContactAlias(val contactId: Long, val localAlias: String): CC() class ApiSetContactAlias(val contactId: Long, val localAlias: String): CC()
class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC() class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC()
class ApiSetUserUIThemes(val userId: Long, val themes: ThemeModeOverrides?): CC()
class ApiSetChatUIThemes(val chatId: String, val themes: ThemeModeOverrides?): CC()
class ApiCreateMyAddress(val userId: Long): CC() class ApiCreateMyAddress(val userId: Long): CC()
class ApiDeleteMyAddress(val userId: Long): CC() class ApiDeleteMyAddress(val userId: Long): CC()
class ApiShowMyAddress(val userId: Long): CC() class ApiShowMyAddress(val userId: Long): CC()
@@ -2574,9 +2598,7 @@ sealed class CC {
is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}" is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}"
is StartChat -> "/_start main=${onOff(mainApp)}" is StartChat -> "/_start main=${onOff(mainApp)}"
is ApiStopChat -> "/_stop" is ApiStopChat -> "/_stop"
is SetTempFolder -> "/_temp_folder $tempFolder" is ApiSetAppFilePaths -> "/set file paths ${json.encodeToString(this)}"
is SetFilesFolder -> "/_files_folder $filesFolder"
is SetRemoteHostsFolder -> "/remote_hosts_folder $remoteHostsFolder"
is ApiSetEncryptLocalFiles -> "/_files_encrypt ${onOff(enable)}" is ApiSetEncryptLocalFiles -> "/_files_encrypt ${onOff(enable)}"
is ApiExportArchive -> "/_db export ${json.encodeToString(config)}" is ApiExportArchive -> "/_db export ${json.encodeToString(config)}"
is ApiImportArchive -> "/_db import ${json.encodeToString(config)}" is ApiImportArchive -> "/_db import ${json.encodeToString(config)}"
@@ -2656,6 +2678,8 @@ sealed class CC {
is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}" is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}"
is ApiSetContactAlias -> "/_set alias @$contactId ${localAlias.trim()}" is ApiSetContactAlias -> "/_set alias @$contactId ${localAlias.trim()}"
is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}" is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}"
is ApiSetUserUIThemes -> "/_set theme user $userId ${if (themes != null) json.encodeToString(themes) else ""}"
is ApiSetChatUIThemes -> "/_set theme $chatId ${if (themes != null) json.encodeToString(themes) else ""}"
is ApiCreateMyAddress -> "/_address $userId" is ApiCreateMyAddress -> "/_address $userId"
is ApiDeleteMyAddress -> "/_delete_address $userId" is ApiDeleteMyAddress -> "/_delete_address $userId"
is ApiShowMyAddress -> "/_show_address $userId" is ApiShowMyAddress -> "/_show_address $userId"
@@ -2720,9 +2744,7 @@ sealed class CC {
is ApiDeleteUser -> "apiDeleteUser" is ApiDeleteUser -> "apiDeleteUser"
is StartChat -> "startChat" is StartChat -> "startChat"
is ApiStopChat -> "apiStopChat" is ApiStopChat -> "apiStopChat"
is SetTempFolder -> "setTempFolder" is ApiSetAppFilePaths -> "apiSetAppFilePaths"
is SetFilesFolder -> "setFilesFolder"
is SetRemoteHostsFolder -> "setRemoteHostsFolder"
is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles" is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles"
is ApiExportArchive -> "apiExportArchive" is ApiExportArchive -> "apiExportArchive"
is ApiImportArchive -> "apiImportArchive" is ApiImportArchive -> "apiImportArchive"
@@ -2790,6 +2812,8 @@ sealed class CC {
is ApiSetContactPrefs -> "apiSetContactPrefs" is ApiSetContactPrefs -> "apiSetContactPrefs"
is ApiSetContactAlias -> "apiSetContactAlias" is ApiSetContactAlias -> "apiSetContactAlias"
is ApiSetConnectionAlias -> "apiSetConnectionAlias" is ApiSetConnectionAlias -> "apiSetConnectionAlias"
is ApiSetUserUIThemes -> "apiSetUserUIThemes"
is ApiSetChatUIThemes -> "apiSetChatUIThemes"
is ApiCreateMyAddress -> "apiCreateMyAddress" is ApiCreateMyAddress -> "apiCreateMyAddress"
is ApiDeleteMyAddress -> "apiDeleteMyAddress" is ApiDeleteMyAddress -> "apiDeleteMyAddress"
is ApiShowMyAddress -> "apiShowMyAddress" is ApiShowMyAddress -> "apiShowMyAddress"
@@ -4061,6 +4085,15 @@ val json = Json {
explicitNulls = false explicitNulls = false
} }
// Can decode unknown enum to default value specified for this field
val jsonCoerceInputValues = Json {
prettyPrint = true
ignoreUnknownKeys = true
encodeDefaults = true
explicitNulls = false
coerceInputValues = true
}
val jsonShort = Json { val jsonShort = Json {
prettyPrint = false prettyPrint = false
ignoreUnknownKeys = true ignoreUnknownKeys = true
@@ -4071,6 +4104,8 @@ val jsonShort = Json {
val yaml = Yaml(configuration = YamlConfiguration( val yaml = Yaml(configuration = YamlConfiguration(
strictMode = false, strictMode = false,
encodeDefaults = false, encodeDefaults = false,
/** ~5.5 MB limitation since wallpaper is limited by 5 MB, see [saveWallpaperFile] */
codePointLimit = 5500000,
)) ))
@Serializable @Serializable
@@ -5521,6 +5556,11 @@ data class AppSettings(
var androidCallOnLockScreen: AppSettingsLockScreenCalls? = null, var androidCallOnLockScreen: AppSettingsLockScreenCalls? = null,
var iosCallKitEnabled: Boolean? = null, var iosCallKitEnabled: Boolean? = null,
var iosCallKitCallsInRecents: Boolean? = null, var iosCallKitCallsInRecents: Boolean? = null,
var uiProfileImageCornerRadius: Float? = null,
var uiColorScheme: String? = null,
var uiDarkColorScheme: String? = null,
var uiCurrentThemeIds: Map<String, String>? = null,
var uiThemes: List<ThemeOverrides>? = null,
) { ) {
fun prepareForExport(): AppSettings { fun prepareForExport(): AppSettings {
val empty = AppSettings() val empty = AppSettings()
@@ -5545,6 +5585,11 @@ data class AppSettings(
if (androidCallOnLockScreen != def.androidCallOnLockScreen) { empty.androidCallOnLockScreen = androidCallOnLockScreen } if (androidCallOnLockScreen != def.androidCallOnLockScreen) { empty.androidCallOnLockScreen = androidCallOnLockScreen }
if (iosCallKitEnabled != def.iosCallKitEnabled) { empty.iosCallKitEnabled = iosCallKitEnabled } if (iosCallKitEnabled != def.iosCallKitEnabled) { empty.iosCallKitEnabled = iosCallKitEnabled }
if (iosCallKitCallsInRecents != def.iosCallKitCallsInRecents) { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents } if (iosCallKitCallsInRecents != def.iosCallKitCallsInRecents) { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents }
if (uiProfileImageCornerRadius != def.uiProfileImageCornerRadius) { empty.uiProfileImageCornerRadius = uiProfileImageCornerRadius }
if (uiColorScheme != def.uiColorScheme) { empty.uiColorScheme = uiColorScheme }
if (uiDarkColorScheme != def.uiDarkColorScheme) { empty.uiDarkColorScheme = uiDarkColorScheme }
if (uiCurrentThemeIds != def.uiCurrentThemeIds) { empty.uiCurrentThemeIds = uiCurrentThemeIds }
if (uiThemes != def.uiThemes) { empty.uiThemes = uiThemes }
return empty return empty
} }
@@ -5577,6 +5622,11 @@ data class AppSettings(
androidCallOnLockScreen?.let { def.callOnLockScreen.set(it.toCallOnLockScreen()) } androidCallOnLockScreen?.let { def.callOnLockScreen.set(it.toCallOnLockScreen()) }
iosCallKitEnabled?.let { def.iosCallKitEnabled.set(it) } iosCallKitEnabled?.let { def.iosCallKitEnabled.set(it) }
iosCallKitCallsInRecents?.let { def.iosCallKitCallsInRecents.set(it) } iosCallKitCallsInRecents?.let { def.iosCallKitCallsInRecents.set(it) }
uiProfileImageCornerRadius?.let { def.profileImageCornerRadius.set(it) }
uiColorScheme?.let { def.currentTheme.set(it) }
uiDarkColorScheme?.let { def.systemDarkTheme.set(it) }
uiCurrentThemeIds?.let { def.currentThemeIds.set(it) }
uiThemes?.let { def.themeOverrides.set(it.skipDuplicates()) }
} }
companion object { companion object {
@@ -5601,7 +5651,12 @@ data class AppSettings(
confirmDBUpgrades = false, confirmDBUpgrades = false,
androidCallOnLockScreen = AppSettingsLockScreenCalls.SHOW, androidCallOnLockScreen = AppSettingsLockScreenCalls.SHOW,
iosCallKitEnabled = true, iosCallKitEnabled = true,
iosCallKitCallsInRecents = false iosCallKitCallsInRecents = false,
uiProfileImageCornerRadius = 22.5f,
uiColorScheme = DefaultTheme.SYSTEM_THEME_NAME,
uiDarkColorScheme = DefaultTheme.SIMPLEX.themeName,
uiCurrentThemeIds = null,
uiThemes = null,
) )
val current: AppSettings val current: AppSettings
@@ -5628,6 +5683,11 @@ data class AppSettings(
androidCallOnLockScreen = AppSettingsLockScreenCalls.from(def.callOnLockScreen.get()), androidCallOnLockScreen = AppSettingsLockScreenCalls.from(def.callOnLockScreen.get()),
iosCallKitEnabled = def.iosCallKitEnabled.get(), iosCallKitEnabled = def.iosCallKitEnabled.get(),
iosCallKitCallsInRecents = def.iosCallKitCallsInRecents.get(), iosCallKitCallsInRecents = def.iosCallKitCallsInRecents.get(),
uiProfileImageCornerRadius = def.profileImageCornerRadius.get(),
uiColorScheme = def.currentTheme.get() ?: DefaultTheme.SYSTEM_THEME_NAME,
uiDarkColorScheme = def.systemDarkTheme.get() ?: DefaultTheme.SIMPLEX.themeName,
uiCurrentThemeIds = def.currentThemeIds.get(),
uiThemes = def.themeOverrides.get(),
) )
} }
} }
@@ -2,7 +2,6 @@ package com.sd.lib.compose.wheel_picker
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import chat.simplex.common.ui.theme.isSystemInDarkTheme
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -12,6 +11,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.ui.theme.isInDarkTheme
/** /**
* The default implementation of focus view in vertical. * The default implementation of focus view in vertical.
@@ -76,7 +76,7 @@ fun FWheelPickerFocusHorizontal(
*/ */
private val DefaultDividerColor: Color private val DefaultDividerColor: Color
@Composable @Composable
get() = (if (isSystemInDarkTheme()) { get() = (if (isInDarkTheme()) {
Color.White Color.White
} else { } else {
Color.Black Color.Black
@@ -42,6 +42,12 @@ fun runMigrations() {
ChatController.appPrefs.currentTheme.set(DefaultTheme.SIMPLEX.name) ChatController.appPrefs.currentTheme.set(DefaultTheme.SIMPLEX.name)
} }
lastMigration.set(117) lastMigration.set(117)
} else if (lastMigration.get() < 203) {
// Moving to a different key for storing themes as a List
val oldOverrides = ChatController.appPrefs.themeOverridesOld.get().values.toList()
ChatController.appPrefs.themeOverrides.set(oldOverrides)
ChatController.appPrefs.currentThemeIds.set(oldOverrides.associate { it.base.themeName to it.themeId })
lastMigration.set(203)
} else { } else {
lastMigration.set(BuildConfigCommon.ANDROID_VERSION_CODE) lastMigration.set(BuildConfigCommon.ANDROID_VERSION_CODE)
break break
@@ -43,14 +43,13 @@ val appPreferences: AppPreferences
val chatController: ChatController = ChatController val chatController: ChatController = ChatController
fun initChatControllerAndRunMigrations() { fun initChatControllerOnStart() {
withLongRunningApi { withLongRunningApi {
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) { if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
initChatController(startChat = ::showStartChatAfterRestartAlert) initChatController(startChat = ::showStartChatAfterRestartAlert)
} else { } else {
initChatController() initChatController()
} }
runMigrations()
} }
} }
@@ -88,11 +87,13 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
return return
} }
platform.androidRestartNetworkObserver() platform.androidRestartNetworkObserver()
controller.apiSetTempFolder(coreTmpDir.absolutePath) controller.apiSetAppFilePaths(
controller.apiSetFilesFolder(appFilesDir.absolutePath) appFilesDir.absolutePath,
if (appPlatform.isDesktop) { coreTmpDir.absolutePath,
controller.apiSetRemoteHostsFolder(remoteHostsDir.absolutePath) wallpapersDir.parentFile.absolutePath,
} remoteHostsDir.absolutePath,
ctrl
)
controller.apiSetEncryptLocalFiles(controller.appPrefs.privacyEncryptLocalFiles.get()) controller.apiSetEncryptLocalFiles(controller.appPrefs.privacyEncryptLocalFiles.get())
// If we migrated successfully means previous re-encryption process on database level finished successfully too // If we migrated successfully means previous re-encryption process on database level finished successfully too
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null) if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
@@ -1,10 +1,12 @@
package chat.simplex.common.platform package chat.simplex.common.platform
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import chat.simplex.common.model.CIFile import chat.simplex.common.model.*
import chat.simplex.common.model.CryptoFile import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.generalGetString import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR import chat.simplex.res.MR
import com.charleskorn.kaml.*
import kotlinx.serialization.encodeToString
import java.io.* import java.io.*
import java.net.URI import java.net.URI
import java.net.URLDecoder import java.net.URLDecoder
@@ -14,8 +16,10 @@ expect val dataDir: File
expect val tmpDir: File expect val tmpDir: File
expect val filesDir: File expect val filesDir: File
expect val appFilesDir: File expect val appFilesDir: File
expect val wallpapersDir: File
expect val coreTmpDir: File expect val coreTmpDir: File
expect val dbAbsolutePrefixPath: String expect val dbAbsolutePrefixPath: String
expect val preferencesDir: File
expect val chatDatabaseFileName: String expect val chatDatabaseFileName: String
expect val agentDatabaseFileName: String expect val agentDatabaseFileName: String
@@ -78,6 +82,20 @@ fun getAppFilePath(fileName: String): String {
} }
} }
fun getWallpaperFilePath(fileName: String): String {
val rh = chatModel.currentRemoteHost.value
val s = File.separator
val path = if (rh == null) {
wallpapersDir.absolutePath + s + fileName
} else {
remoteHostsDir.absolutePath + s + rh.storePath + s + "simplex_v1_assets" + s + "wallpapers" + s + fileName
}
File(path).parentFile.mkdirs()
return path
}
fun getPreferenceFilePath(fileName: String = "themes.yaml"): String = preferencesDir.absolutePath + File.separator + fileName
fun getLoadedFilePath(file: CIFile?): String? { fun getLoadedFilePath(file: CIFile?): String? {
val f = file?.fileSource?.filePath val f = file?.fileSource?.filePath
return if (f != null && file.loaded) { return if (f != null && file.loaded) {
@@ -98,6 +116,42 @@ fun getLoadedFileSource(file: CIFile?): CryptoFile? {
} }
} }
fun readThemeOverrides(): List<ThemeOverrides> {
return try {
val file = File(getPreferenceFilePath("themes.yaml"))
if (!file.exists()) return emptyList()
file.inputStream().use {
val map = yaml.parseToYamlNode(it).yamlMap
val list = map.get<YamlList>("themes")
val res = ArrayList<ThemeOverrides>()
list?.items?.forEach {
try {
res.add(yaml.decodeFromYamlNode(ThemeOverrides.serializer(), it))
} catch (e: Throwable) {
Log.e(TAG, "Error while reading specific theme: ${e.stackTraceToString()}")
}
}
res.skipDuplicates()
}
} catch (e: Throwable) {
Log.e(TAG, "Error while reading themes file: ${e.stackTraceToString()}")
emptyList()
}
}
fun writeThemeOverrides(overrides: List<ThemeOverrides>): Boolean =
try {
File(getPreferenceFilePath("themes.yaml")).outputStream().use {
val string = yaml.encodeToString(ThemesFile(themes = overrides))
it.bufferedWriter().use { it.write(string) }
}
true
} catch (e: Throwable) {
Log.e(TAG, "Error while writing themes file: ${e.stackTraceToString()}")
false
}
private fun fileReady(file: CIFile, filePath: String) = private fun fileReady(file: CIFile, filePath: String) =
File(filePath).exists() && File(filePath).exists() &&
CIFile.cachedRemoteFileRequests[file.fileSource] != false CIFile.cachedRemoteFileRequests[file.fileSource] != false
@@ -1,11 +1,13 @@
package chat.simplex.common.platform package chat.simplex.common.platform
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
@Composable @Composable
@@ -31,3 +33,5 @@ expect fun windowWidth(): Dp
expect fun desktopExpandWindowToWidth(width: Dp) expect fun desktopExpandWindowToWidth(width: Dp)
expect fun isRtl(text: CharSequence): Boolean expect fun isRtl(text: CharSequence): Boolean
expect fun ImageResource.toComposeImageBitmap(): ImageBitmap?
@@ -31,17 +31,6 @@ val WarningOrange = Color(255, 127, 0, 255)
val WarningYellow = Color(255, 192, 0, 255) val WarningYellow = Color(255, 192, 0, 255)
val FileLight = Color(183, 190, 199, 255) val FileLight = Color(183, 190, 199, 255)
val FileDark = Color(101, 101, 106, 255) val FileDark = Color(101, 101, 106, 255)
val SentMessageColor = Color(0x1E45B8FF)
val MenuTextColor: Color @Composable get () = if (isInDarkTheme()) LocalContentColor.current.copy(alpha = 0.8f) else Color.Black val MenuTextColor: Color @Composable get () = if (isInDarkTheme()) LocalContentColor.current.copy(alpha = 0.8f) else Color.Black
val NoteFolderIconColor: Color @Composable get() = with(CurrentColors.collectAsState().value.appColors.sentMessage) { val NoteFolderIconColor: Color @Composable get() = MaterialTheme.appColors.primaryVariant2
// Default color looks too light and better to have it here a little bit brighter
if (alpha == SentMessageColor.alpha) {
copy(min(SentMessageColor.alpha + 0.1f, 1f))
} else {
// Color is non-standard and theme maker can choose color without alpha at all since the theme bound to dark/light variant,
// and it shouldn't be universal
this
}
}
@@ -6,53 +6,136 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ChatController import chat.simplex.common.model.ChatController
import chat.simplex.common.platform.isInNightMode import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex
import chat.simplex.common.ui.theme.ThemeManager.toReadableHex
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.serialization.Transient
import java.util.UUID
enum class DefaultTheme { enum class DefaultTheme {
SYSTEM, LIGHT, DARK, SIMPLEX; LIGHT, DARK, SIMPLEX, BLACK;
companion object {
const val SYSTEM_THEME_NAME: String = "SYSTEM"
}
val themeName: String
get() = name
val mode: DefaultThemeMode get() = if (this == LIGHT) DefaultThemeMode.LIGHT else DefaultThemeMode.DARK
// Call it only with base theme, not SYSTEM // Call it only with base theme, not SYSTEM
fun hasChangedAnyColor(colors: Colors, appColors: AppColors): Boolean { fun hasChangedAnyColor(overrides: ThemeOverrides?): Boolean {
val palette = when (this) { if (overrides == null) return false
SYSTEM -> return false return overrides.colors != ThemeColors() ||
LIGHT -> LightColorPalette overrides.wallpaper != null && (overrides.wallpaper.background != null || overrides.wallpaper.tint != null)
DARK -> DarkColorPalette
SIMPLEX -> SimplexColorPalette
}
val appPalette = when (this) {
SYSTEM -> return false
LIGHT -> LightColorPaletteApp
DARK -> DarkColorPaletteApp
SIMPLEX -> SimplexColorPaletteApp
}
return colors.primary != palette.primary ||
colors.primaryVariant != palette.primaryVariant ||
colors.secondary != palette.secondary ||
colors.secondaryVariant != palette.secondaryVariant ||
colors.background != palette.background ||
colors.surface != palette.surface ||
appColors != appPalette
} }
} }
data class AppColors( @Serializable
val title: Color, enum class DefaultThemeMode {
val sentMessage: Color, @SerialName("light") LIGHT,
val receivedMessage: Color @SerialName("dark") DARK
) }
@Stable
class AppColors(
title: Color,
primaryVariant2: Color,
sentMessage: Color,
sentQuote: Color,
receivedMessage: Color,
receivedQuote: Color,
) {
var title by mutableStateOf(title, structuralEqualityPolicy())
internal set
var primaryVariant2 by mutableStateOf(primaryVariant2, structuralEqualityPolicy())
internal set
var sentMessage by mutableStateOf(sentMessage, structuralEqualityPolicy())
internal set
var sentQuote by mutableStateOf(sentQuote, structuralEqualityPolicy())
internal set
var receivedMessage by mutableStateOf(receivedMessage, structuralEqualityPolicy())
internal set
var receivedQuote by mutableStateOf(receivedQuote, structuralEqualityPolicy())
internal set
fun copy(
title: Color = this.title,
primaryVariant2: Color = this.primaryVariant2,
sentMessage: Color = this.sentMessage,
sentQuote: Color = this.sentQuote,
receivedMessage: Color = this.receivedMessage,
receivedQuote: Color = this.receivedQuote,
): AppColors = AppColors(
title,
primaryVariant2,
sentMessage,
sentQuote,
receivedMessage,
receivedQuote,
)
override fun toString(): String {
return buildString {
append("AppColors(")
append("title=$title, ")
append("primaryVariant2=$primaryVariant2, ")
append("sentMessage=$sentMessage, ")
append("sentQuote=$sentQuote, ")
append("receivedMessage=$receivedMessage, ")
append("receivedQuote=$receivedQuote")
append(")")
}
}
}
@Stable
class AppWallpaper(
background: Color? = null,
tint: Color? = null,
type: WallpaperType = WallpaperType.Empty,
) {
var background by mutableStateOf(background, structuralEqualityPolicy())
internal set
var tint by mutableStateOf(tint, structuralEqualityPolicy())
internal set
var type by mutableStateOf(type, structuralEqualityPolicy())
internal set
fun copy(
background: Color? = this.background,
tint: Color? = this.tint,
type: WallpaperType = this.type,
): AppWallpaper = AppWallpaper(
background,
tint,
type,
)
override fun toString(): String {
return buildString {
append("AppWallpaper(")
append("background=$background, ")
append("tint=$tint, ")
append("type=$type")
append(")")
}
}
}
enum class ThemeColor { enum class ThemeColor {
PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE, TITLE, SENT_MESSAGE, RECEIVED_MESSAGE; PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE, TITLE, SENT_MESSAGE, SENT_QUOTE, RECEIVED_MESSAGE, RECEIVED_QUOTE, PRIMARY_VARIANT2, WALLPAPER_BACKGROUND, WALLPAPER_TINT;
fun fromColors(colors: Colors, appColors: AppColors): Color { fun fromColors(colors: Colors, appColors: AppColors, appWallpaper: AppWallpaper): Color? {
return when (this) { return when (this) {
PRIMARY -> colors.primary PRIMARY -> colors.primary
PRIMARY_VARIANT -> colors.primaryVariant PRIMARY_VARIANT -> colors.primaryVariant
@@ -61,8 +144,13 @@ enum class ThemeColor {
BACKGROUND -> colors.background BACKGROUND -> colors.background
SURFACE -> colors.surface SURFACE -> colors.surface
TITLE -> appColors.title TITLE -> appColors.title
PRIMARY_VARIANT2 -> appColors.primaryVariant2
SENT_MESSAGE -> appColors.sentMessage SENT_MESSAGE -> appColors.sentMessage
SENT_QUOTE -> appColors.sentQuote
RECEIVED_MESSAGE -> appColors.receivedMessage RECEIVED_MESSAGE -> appColors.receivedMessage
RECEIVED_QUOTE -> appColors.receivedQuote
WALLPAPER_BACKGROUND -> appWallpaper.background
WALLPAPER_TINT -> appWallpaper.tint
} }
} }
@@ -75,8 +163,13 @@ enum class ThemeColor {
BACKGROUND -> generalGetString(MR.strings.color_background) BACKGROUND -> generalGetString(MR.strings.color_background)
SURFACE -> generalGetString(MR.strings.color_surface) SURFACE -> generalGetString(MR.strings.color_surface)
TITLE -> generalGetString(MR.strings.color_title) TITLE -> generalGetString(MR.strings.color_title)
PRIMARY_VARIANT2 -> generalGetString(MR.strings.color_primary_variant2)
SENT_MESSAGE -> generalGetString(MR.strings.color_sent_message) SENT_MESSAGE -> generalGetString(MR.strings.color_sent_message)
SENT_QUOTE -> generalGetString(MR.strings.color_sent_quote)
RECEIVED_MESSAGE -> generalGetString(MR.strings.color_received_message) RECEIVED_MESSAGE -> generalGetString(MR.strings.color_received_message)
RECEIVED_QUOTE -> generalGetString(MR.strings.color_received_quote)
WALLPAPER_BACKGROUND -> generalGetString(MR.strings.color_wallpaper_background)
WALLPAPER_TINT -> generalGetString(MR.strings.color_wallpaper_tint)
} }
} }
@@ -92,45 +185,232 @@ data class ThemeColors(
@SerialName("menus") @SerialName("menus")
val surface: String? = null, val surface: String? = null,
val title: String? = null, val title: String? = null,
@SerialName("accentVariant2")
val primaryVariant2: String? = null,
val sentMessage: String? = null, val sentMessage: String? = null,
@SerialName("sentReply")
val sentQuote: String? = null,
val receivedMessage: String? = null, val receivedMessage: String? = null,
@SerialName("receivedReply")
val receivedQuote: String? = null,
) { ) {
fun toColors(base: DefaultTheme): Colors { companion object {
fun from(colors: Colors, appColors: AppColors): ThemeColors =
ThemeColors(
primary = colors.primary.toReadableHex(),
primaryVariant = colors.primaryVariant.toReadableHex(),
secondary = colors.secondary.toReadableHex(),
secondaryVariant = colors.secondaryVariant.toReadableHex(),
background = colors.background.toReadableHex(),
surface = colors.surface.toReadableHex(),
title = appColors.title.toReadableHex(),
primaryVariant2 = appColors.primaryVariant2.toReadableHex(),
sentMessage = appColors.sentMessage.toReadableHex(),
sentQuote = appColors.sentQuote.toReadableHex(),
receivedMessage = appColors.receivedMessage.toReadableHex(),
receivedQuote = appColors.receivedQuote.toReadableHex(),
)
}
}
@Serializable
data class ThemeWallpaper (
val preset: String? = null,
val scale: Float? = null,
val scaleType: WallpaperScaleType? = null,
val background: String? = null,
val tint: String? = null,
val image: String? = null,
val imageFile: String? = null,
) {
fun toAppWallpaper(): AppWallpaper {
return AppWallpaper(
background = background?.colorFromReadableHex(),
tint = tint?.colorFromReadableHex(),
type = WallpaperType.from(this) ?: WallpaperType.Empty
)
}
fun withFilledWallpaperBase64(): ThemeWallpaper {
val aw = toAppWallpaper()
val type = aw.type
return ThemeWallpaper(
image = if (type is WallpaperType.Image && type.image != null) resizeImageToStrSize(type.image!!, 5_000_000) else null,
imageFile = null,
preset = if (type is WallpaperType.Preset) type.filename else null,
scale = if (type is WallpaperType.Preset) type.scale else if (type is WallpaperType.Image) type.scale else 1f,
scaleType = if (type is WallpaperType.Image) type.scaleType else null,
background = aw.background?.toReadableHex(),
tint = aw.tint?.toReadableHex(),
)
}
fun withFilledWallpaperPath(): ThemeWallpaper {
val aw = toAppWallpaper()
val type = aw.type
return ThemeWallpaper(
image = null,
imageFile = if (type is WallpaperType.Image) type.filename else null,
preset = if (type is WallpaperType.Preset) type.filename else null,
scale = if (scale == null) null else if (type is WallpaperType.Preset) type.scale else if (type is WallpaperType.Image) scale else null,
scaleType = if (scaleType == null) null else if (type is WallpaperType.Image) type.scaleType else null,
background = aw.background?.toReadableHex(),
tint = aw.tint?.toReadableHex(),
)
}
fun importFromString(): ThemeWallpaper =
if (preset == null && image != null) {
// Need to save image from string and to save its path
try {
val parsed = base64ToBitmap(image)
val filename = saveWallpaperFile(parsed)
copy(image = null, imageFile = filename)
} catch (e: Exception) {
Log.e(TAG, "Error while parsing/copying the image: ${e.stackTraceToString()}")
ThemeWallpaper()
}
} else this
companion object {
fun from(type: WallpaperType, background: String?, tint: String?): ThemeWallpaper {
return ThemeWallpaper(
image = null,
imageFile = if (type is WallpaperType.Image) type.filename else null,
preset = if (type is WallpaperType.Preset) type.filename else null,
scale = if (type is WallpaperType.Preset) type.scale else if (type is WallpaperType.Image) type.scale else null,
scaleType = if (type is WallpaperType.Image) type.scaleType else null,
background = background,
tint = tint,
)
}
}
}
@Serializable
data class ThemesFile(
val themes: List<ThemeOverrides> = emptyList()
)
@Serializable
data class ThemeOverrides (
val themeId: String = UUID.randomUUID().toString(),
val base: DefaultTheme,
val colors: ThemeColors = ThemeColors(),
val wallpaper: ThemeWallpaper? = null,
) {
fun isSame(type: WallpaperType?, themeName: String): Boolean =
(
(wallpaper?.preset != null && type is WallpaperType.Preset && wallpaper.preset == type.filename) ||
(wallpaper?.imageFile != null && type is WallpaperType.Image) ||
(wallpaper?.preset == null && wallpaper?.imageFile == null && (type == WallpaperType.Empty || type == null))
) && base.themeName == themeName
fun withUpdatedColor(name: ThemeColor, color: String?): ThemeOverrides {
return copy(
colors = when (name) {
ThemeColor.PRIMARY -> colors.copy(primary = color)
ThemeColor.PRIMARY_VARIANT -> colors.copy(primaryVariant = color)
ThemeColor.SECONDARY -> colors.copy(secondary = color)
ThemeColor.SECONDARY_VARIANT -> colors.copy(secondaryVariant = color)
ThemeColor.BACKGROUND -> colors.copy(background = color)
ThemeColor.SURFACE -> colors.copy(surface = color)
ThemeColor.TITLE -> colors.copy(title = color)
ThemeColor.PRIMARY_VARIANT2 -> colors.copy(primaryVariant2 = color)
ThemeColor.SENT_MESSAGE -> colors.copy(sentMessage = color)
ThemeColor.SENT_QUOTE -> colors.copy(sentQuote = color)
ThemeColor.RECEIVED_MESSAGE -> colors.copy(receivedMessage = color)
ThemeColor.RECEIVED_QUOTE -> colors.copy(receivedQuote = color)
ThemeColor.WALLPAPER_BACKGROUND -> colors.copy()
ThemeColor.WALLPAPER_TINT -> colors.copy()
}, wallpaper = when (name) {
ThemeColor.WALLPAPER_BACKGROUND -> wallpaper?.copy(background = color)
ThemeColor.WALLPAPER_TINT -> wallpaper?.copy(tint = color)
else -> wallpaper?.copy()
}
)
}
fun toColors(base: DefaultTheme, perChatTheme: ThemeColors?, perUserTheme: ThemeColors?, presetWallpaperTheme: ThemeColors?): Colors {
val baseColors = when (base) { val baseColors = when (base) {
DefaultTheme.LIGHT -> LightColorPalette DefaultTheme.LIGHT -> LightColorPalette
DefaultTheme.DARK -> DarkColorPalette DefaultTheme.DARK -> DarkColorPalette
DefaultTheme.SIMPLEX -> SimplexColorPalette DefaultTheme.SIMPLEX -> SimplexColorPalette
// shouldn't be here DefaultTheme.BLACK -> BlackColorPalette
DefaultTheme.SYSTEM -> LightColorPalette
} }
return baseColors.copy( return baseColors.copy(
primary = primary?.colorFromReadableHex() ?: baseColors.primary, primary = perChatTheme?.primary?.colorFromReadableHex() ?: perUserTheme?.primary?.colorFromReadableHex() ?: colors.primary?.colorFromReadableHex() ?: presetWallpaperTheme?.primary?.colorFromReadableHex() ?: baseColors.primary,
primaryVariant = primaryVariant?.colorFromReadableHex() ?: baseColors.primaryVariant, primaryVariant = perChatTheme?.primaryVariant?.colorFromReadableHex() ?: perUserTheme?.primaryVariant?.colorFromReadableHex() ?: colors.primaryVariant?.colorFromReadableHex() ?: presetWallpaperTheme?.primaryVariant?.colorFromReadableHex() ?: baseColors.primaryVariant,
secondary = secondary?.colorFromReadableHex() ?: baseColors.secondary, secondary = perChatTheme?.secondary?.colorFromReadableHex() ?: perUserTheme?.secondary?.colorFromReadableHex() ?: colors.secondary?.colorFromReadableHex() ?: presetWallpaperTheme?.secondary?.colorFromReadableHex() ?: baseColors.secondary,
secondaryVariant = secondaryVariant?.colorFromReadableHex() ?: baseColors.secondaryVariant, secondaryVariant = perChatTheme?.secondaryVariant?.colorFromReadableHex() ?: perUserTheme?.secondaryVariant?.colorFromReadableHex() ?: colors.secondaryVariant?.colorFromReadableHex() ?: presetWallpaperTheme?.secondaryVariant?.colorFromReadableHex() ?: baseColors.secondaryVariant,
background = background?.colorFromReadableHex() ?: baseColors.background, background = perChatTheme?.background?.colorFromReadableHex() ?: perUserTheme?.background?.colorFromReadableHex() ?: colors.background?.colorFromReadableHex() ?: presetWallpaperTheme?.background?.colorFromReadableHex() ?: baseColors.background,
surface = surface?.colorFromReadableHex() ?: baseColors.surface, surface = perChatTheme?.surface?.colorFromReadableHex() ?: perUserTheme?.surface?.colorFromReadableHex() ?: colors.surface?.colorFromReadableHex() ?: presetWallpaperTheme?.surface?.colorFromReadableHex() ?: baseColors.surface,
) )
} }
fun toAppColors(base: DefaultTheme): AppColors { fun toAppColors(base: DefaultTheme, perChatTheme: ThemeColors?, perChatWallpaperType: WallpaperType?, perUserTheme: ThemeColors?, perUserWallpaperType: WallpaperType?, presetWallpaperTheme: ThemeColors?): AppColors {
val baseColors = when (base) { val baseColors = when (base) {
DefaultTheme.LIGHT -> LightColorPaletteApp DefaultTheme.LIGHT -> LightColorPaletteApp
DefaultTheme.DARK -> DarkColorPaletteApp DefaultTheme.DARK -> DarkColorPaletteApp
DefaultTheme.SIMPLEX -> SimplexColorPaletteApp DefaultTheme.SIMPLEX -> SimplexColorPaletteApp
// shouldn't be here DefaultTheme.BLACK -> BlackColorPaletteApp
DefaultTheme.SYSTEM -> LightColorPaletteApp
} }
val sentMessageFallback = colors.sentMessage?.colorFromReadableHex() ?: presetWallpaperTheme?.sentMessage?.colorFromReadableHex() ?: baseColors.sentMessage
val sentQuoteFallback = colors.sentQuote?.colorFromReadableHex() ?: presetWallpaperTheme?.sentQuote?.colorFromReadableHex() ?: baseColors.sentQuote
val receivedMessageFallback = colors.receivedMessage?.colorFromReadableHex() ?: presetWallpaperTheme?.receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage
val receivedQuoteFallback = colors.receivedQuote?.colorFromReadableHex() ?: presetWallpaperTheme?.receivedQuote?.colorFromReadableHex() ?: baseColors.receivedQuote
return baseColors.copy( return baseColors.copy(
title = title?.colorFromReadableHex() ?: baseColors.title, title = perChatTheme?.title?.colorFromReadableHex() ?: perUserTheme?.title?.colorFromReadableHex() ?: colors.title?.colorFromReadableHex() ?: presetWallpaperTheme?.title?.colorFromReadableHex() ?: baseColors.title,
sentMessage = sentMessage?.colorFromReadableHex() ?: baseColors.sentMessage, primaryVariant2 = perChatTheme?.primaryVariant2?.colorFromReadableHex() ?: perUserTheme?.primaryVariant2?.colorFromReadableHex() ?: colors.primaryVariant2?.colorFromReadableHex() ?: presetWallpaperTheme?.primaryVariant2?.colorFromReadableHex() ?: baseColors.primaryVariant2,
receivedMessage = receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage, sentMessage = if (perChatTheme?.sentMessage != null) perChatTheme.sentMessage.colorFromReadableHex()
else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.sentMessage?.colorFromReadableHex() ?: sentMessageFallback
else sentMessageFallback,
sentQuote = if (perChatTheme?.sentQuote != null) perChatTheme.sentQuote.colorFromReadableHex()
else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.sentQuote?.colorFromReadableHex() ?: sentQuoteFallback
else sentQuoteFallback,
receivedMessage = if (perChatTheme?.receivedMessage != null) perChatTheme.receivedMessage.colorFromReadableHex()
else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.receivedMessage?.colorFromReadableHex() ?: receivedMessageFallback
else receivedMessageFallback,
receivedQuote = if (perChatTheme?.receivedQuote != null) perChatTheme.receivedQuote.colorFromReadableHex()
else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.receivedQuote?.colorFromReadableHex() ?: receivedQuoteFallback
else receivedQuoteFallback,
) )
} }
fun withFilledColors(base: DefaultTheme): ThemeColors { fun toAppWallpaper(themeOverridesForType: WallpaperType?, perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverride?, materialBackgroundColor: Color): AppWallpaper {
val c = toColors(base) val mainType = when {
val ac = toAppColors(base) themeOverridesForType != null -> themeOverridesForType
// type can be null if override is empty `"wallpaper": "{}"`, in this case no wallpaper is needed, empty.
// It's not null to override upper level wallpaper
perChatTheme?.wallpaper != null -> perChatTheme.wallpaper.toAppWallpaper().type
perUserTheme?.wallpaper != null -> perUserTheme.wallpaper.toAppWallpaper().type
else -> wallpaper?.toAppWallpaper()?.type ?: return AppWallpaper()
}
val first: ThemeWallpaper? = if (mainType.sameType(perChatTheme?.wallpaper?.toAppWallpaper()?.type)) perChatTheme?.wallpaper else null
val second: ThemeWallpaper? = if (mainType.sameType(perUserTheme?.wallpaper?.toAppWallpaper()?.type)) perUserTheme?.wallpaper else null
val third: ThemeWallpaper? = if (mainType.sameType(this.wallpaper?.toAppWallpaper()?.type)) this.wallpaper else null
return AppWallpaper(type = when (mainType) {
is WallpaperType.Preset -> mainType.copy(
scale = mainType.scale ?: first?.scale ?: second?.scale ?: third?.scale
)
is WallpaperType.Image -> mainType.copy(
scale = if (themeOverridesForType == null) mainType.scale ?: first?.scale ?: second?.scale ?: third?.scale else second?.scale ?: third?.scale ?: mainType.scale,
scaleType = if (themeOverridesForType == null) mainType.scaleType ?: first?.scaleType ?: second?.scaleType ?: third?.scaleType else second?.scaleType ?: third?.scaleType ?: mainType.scaleType,
filename = if (themeOverridesForType == null) mainType.filename else first?.imageFile ?: second?.imageFile ?: third?.imageFile ?: mainType.filename,
)
is WallpaperType.Empty -> mainType
},
background = (first?.background ?: second?.background ?: third?.background)?.colorFromReadableHex() ?: mainType.defaultBackgroundColor(base, materialBackgroundColor),
tint = (first?.tint ?: second?.tint ?: third?.tint)?.colorFromReadableHex() ?: mainType.defaultTintColor(base)
)
}
fun withFilledColors(base: DefaultTheme, perChatTheme: ThemeColors?, perChatWallpaperType: WallpaperType?, perUserTheme: ThemeColors?, perUserWallpaperType: WallpaperType?, presetWallpaperTheme: ThemeColors?): ThemeColors {
val c = toColors(base, perChatTheme, perUserTheme, presetWallpaperTheme)
val ac = toAppColors(base, perChatTheme, perChatWallpaperType, perUserTheme, perUserWallpaperType, presetWallpaperTheme)
return ThemeColors( return ThemeColors(
primary = c.primary.toReadableHex(), primary = c.primary.toReadableHex(),
primaryVariant = c.primaryVariant.toReadableHex(), primaryVariant = c.primaryVariant.toReadableHex(),
@@ -139,23 +419,71 @@ data class ThemeColors(
background = c.background.toReadableHex(), background = c.background.toReadableHex(),
surface = c.surface.toReadableHex(), surface = c.surface.toReadableHex(),
title = ac.title.toReadableHex(), title = ac.title.toReadableHex(),
primaryVariant2 = ac.primaryVariant2.toReadableHex(),
sentMessage = ac.sentMessage.toReadableHex(), sentMessage = ac.sentMessage.toReadableHex(),
receivedMessage = ac.receivedMessage.toReadableHex() sentQuote = ac.sentQuote.toReadableHex(),
receivedMessage = ac.receivedMessage.toReadableHex(),
receivedQuote = ac.receivedQuote.toReadableHex(),
) )
} }
} }
private fun String.colorFromReadableHex(): Color = fun List<ThemeOverrides>.getTheme(themeId: String?): ThemeOverrides? =
Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong()) firstOrNull { it.themeId == themeId }
private fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb()) fun List<ThemeOverrides>.getTheme(themeId: String?, type: WallpaperType?, base: DefaultTheme): ThemeOverrides? =
firstOrNull { it.themeId == themeId || it.isSame(type, base.themeName)}
fun List<ThemeOverrides>.replace(theme: ThemeOverrides): List<ThemeOverrides> {
val index = indexOfFirst { it.themeId == theme.themeId ||
// prevent situation when two themes has the same type but different theme id (maybe something was changed in prefs by hand)
it.isSame(WallpaperType.from(theme.wallpaper), theme.base.themeName)
}
return if (index != -1) {
val a = ArrayList(this)
a[index] = theme
a
} else {
this + theme
}
}
fun List<ThemeOverrides>.sameTheme(type: WallpaperType?, themeName: String): ThemeOverrides? = firstOrNull { it.isSame(type, themeName) }
/** See [ThemesTest.testSkipDuplicates] */
fun List<ThemeOverrides>.skipDuplicates(): List<ThemeOverrides> {
val res = ArrayList<ThemeOverrides>()
forEach { theme ->
val themeType = WallpaperType.from(theme.wallpaper)
if (res.none { it.themeId == theme.themeId || it.isSame(themeType, theme.base.themeName) }) {
res.add(theme)
}
}
return res
}
@Serializable @Serializable
data class ThemeOverrides ( data class ThemeModeOverrides (
val base: DefaultTheme, val light: ThemeModeOverride? = null,
val colors: ThemeColors val dark: ThemeModeOverride? = null
) { ) {
fun withUpdatedColor(name: ThemeColor, color: String): ThemeOverrides { fun preferredMode(darkTheme: Boolean): ThemeModeOverride? = when (darkTheme) {
false -> light
else -> dark
}
}
@Serializable
data class ThemeModeOverride (
val mode: DefaultThemeMode = CurrentColors.value.base.mode,
val colors: ThemeColors = ThemeColors(),
val wallpaper: ThemeWallpaper? = null,
) {
@Transient
val type = WallpaperType.from(wallpaper)
fun withUpdatedColor(name: ThemeColor, color: String?): ThemeModeOverride {
return copy(colors = when (name) { return copy(colors = when (name) {
ThemeColor.PRIMARY -> colors.copy(primary = color) ThemeColor.PRIMARY -> colors.copy(primary = color)
ThemeColor.PRIMARY_VARIANT -> colors.copy(primaryVariant = color) ThemeColor.PRIMARY_VARIANT -> colors.copy(primaryVariant = color)
@@ -164,9 +492,97 @@ data class ThemeOverrides (
ThemeColor.BACKGROUND -> colors.copy(background = color) ThemeColor.BACKGROUND -> colors.copy(background = color)
ThemeColor.SURFACE -> colors.copy(surface = color) ThemeColor.SURFACE -> colors.copy(surface = color)
ThemeColor.TITLE -> colors.copy(title = color) ThemeColor.TITLE -> colors.copy(title = color)
ThemeColor.PRIMARY_VARIANT2 -> colors.copy(primaryVariant2 = color)
ThemeColor.SENT_MESSAGE -> colors.copy(sentMessage = color) ThemeColor.SENT_MESSAGE -> colors.copy(sentMessage = color)
ThemeColor.SENT_QUOTE -> colors.copy(sentQuote = color)
ThemeColor.RECEIVED_MESSAGE -> colors.copy(receivedMessage = color) ThemeColor.RECEIVED_MESSAGE -> colors.copy(receivedMessage = color)
}) ThemeColor.RECEIVED_QUOTE -> colors.copy(receivedQuote = color)
ThemeColor.WALLPAPER_BACKGROUND -> colors.copy()
ThemeColor.WALLPAPER_TINT -> colors.copy()
}, wallpaper = when (name) {
ThemeColor.WALLPAPER_BACKGROUND -> wallpaper?.copy(background = color)
ThemeColor.WALLPAPER_TINT -> wallpaper?.copy(tint = color)
else -> wallpaper?.copy()
}
)
}
fun removeSameColors(base: DefaultTheme): ThemeModeOverride {
val c = when (base) {
DefaultTheme.LIGHT -> LightColorPalette
DefaultTheme.DARK -> DarkColorPalette
DefaultTheme.SIMPLEX -> SimplexColorPalette
DefaultTheme.BLACK -> BlackColorPalette
}
val ac = when (base) {
DefaultTheme.LIGHT -> LightColorPaletteApp
DefaultTheme.DARK -> DarkColorPaletteApp
DefaultTheme.SIMPLEX -> SimplexColorPaletteApp
DefaultTheme.BLACK -> BlackColorPaletteApp
}
val w = when (val wallpaperType = WallpaperType.from(wallpaper)) {
is WallpaperType.Preset -> {
val p = PresetWallpaper.from(wallpaperType.filename)
ThemeWallpaper(
preset = wallpaperType.filename,
scale = p?.scale ?: wallpaper?.scale,
scaleType = null,
background = p?.background?.get(base)?.toReadableHex(),
tint = p?.tint?.get(base)?.toReadableHex(),
image = null,
imageFile = null,
)
}
is WallpaperType.Image -> {
ThemeWallpaper(
preset = null,
scale = null,
scaleType = WallpaperScaleType.FILL,
background = Color.Transparent.toReadableHex(),
tint = Color.Transparent.toReadableHex(),
image = null,
imageFile = null,
)
}
else -> {
ThemeWallpaper()
}
}
return copy(
colors = ThemeColors(
primary = if (colors.primary?.colorFromReadableHex() != c.primary) colors.primary else null,
primaryVariant = if (colors.primaryVariant?.colorFromReadableHex() != c.primaryVariant) colors.primaryVariant else null,
secondary = if (colors.secondary?.colorFromReadableHex() != c.secondary) colors.secondary else null,
secondaryVariant = if (colors.secondaryVariant?.colorFromReadableHex() != c.secondaryVariant) colors.secondaryVariant else null,
background = if (colors.background?.colorFromReadableHex() != c.background) colors.background else null,
surface = if (colors.surface?.colorFromReadableHex() != c.surface) colors.surface else null,
title = if (colors.title?.colorFromReadableHex() != ac.title) colors.title else null,
primaryVariant2 = if (colors.primaryVariant2?.colorFromReadableHex() != ac.primaryVariant2) colors.primary else null,
sentMessage = if (colors.sentMessage?.colorFromReadableHex() != ac.sentMessage) colors.sentMessage else null,
sentQuote = if (colors.sentQuote?.colorFromReadableHex() != ac.sentQuote) colors.sentQuote else null,
receivedMessage = if (colors.receivedMessage?.colorFromReadableHex() != ac.receivedMessage) colors.receivedMessage else null,
receivedQuote = if (colors.receivedQuote?.colorFromReadableHex() != ac.receivedQuote) colors.receivedQuote else null,
),
wallpaper = wallpaper?.copy(
preset = wallpaper.preset,
scale = if (wallpaper.scale != w.scale) wallpaper.scale else null,
scaleType = if (wallpaper.scaleType != w.scaleType) wallpaper.scaleType else null,
background = if (wallpaper.background != w.background) wallpaper.background else null,
tint = if (wallpaper.tint != w.tint) wallpaper.tint else null,
image = wallpaper.image,
imageFile = wallpaper.imageFile,
)
)
}
companion object {
fun withFilledAppDefaults(mode: DefaultThemeMode, base: DefaultTheme): ThemeModeOverride =
ThemeModeOverride(
mode = mode,
colors = ThemeOverrides(base = base).withFilledColors(base, null, null, null, null, null),
wallpaper = ThemeWallpaper(preset = PresetWallpaper.SCHOOL.filename)
)
} }
} }
@@ -204,7 +620,6 @@ val DarkColorPalette = darkColors(
// background = Color.Black, // background = Color.Black,
surface = Color(0xFF222222), surface = Color(0xFF222222),
// background = Color(0xFF121212), // background = Color(0xFF121212),
// surface = Color(0xFF121212),
error = Color.Red, error = Color.Red,
onBackground = Color(0xFFFFFBFA), onBackground = Color(0xFFFFFBFA),
onSurface = Color(0xFFFFFBFA), onSurface = Color(0xFFFFFBFA),
@@ -212,8 +627,11 @@ val DarkColorPalette = darkColors(
) )
val DarkColorPaletteApp = AppColors( val DarkColorPaletteApp = AppColors(
title = SimplexBlue, title = SimplexBlue,
sentMessage = SentMessageColor, primaryVariant2 = Color(0xFF18262E),
receivedMessage = Color(0x20B1B0B5) sentMessage = Color(0xFF18262E),
sentQuote = Color(0xFF1D3847),
receivedMessage = Color(0xff262627),
receivedQuote = Color(0xff373739),
) )
val LightColorPalette = lightColors( val LightColorPalette = lightColors(
@@ -231,8 +649,11 @@ val LightColorPalette = lightColors(
) )
val LightColorPaletteApp = AppColors( val LightColorPaletteApp = AppColors(
title = SimplexBlue, title = SimplexBlue,
sentMessage = SentMessageColor, primaryVariant2 = Color(0xFFE9F7FF),
receivedMessage = Color(0x20B1B0B5) sentMessage = Color(0xFFE9F7FF),
sentQuote = Color(0xFFD6F0FF),
receivedMessage = Color(0xfff5f5f6),
receivedQuote = Color(0xffececee),
) )
val SimplexColorPalette = darkColors( val SimplexColorPalette = darkColors(
@@ -251,11 +672,39 @@ val SimplexColorPalette = darkColors(
) )
val SimplexColorPaletteApp = AppColors( val SimplexColorPaletteApp = AppColors(
title = Color(0xFF267BE5), title = Color(0xFF267BE5),
sentMessage = SentMessageColor, primaryVariant2 = Color(0xFF172941),
receivedMessage = Color(0x20B1B0B5) sentMessage = Color(0xFF172941),
sentQuote = Color(0xFF1C3A57),
receivedMessage = Color(0xff25283a),
receivedQuote = Color(0xff36394a),
) )
val CurrentColors: MutableStateFlow<ThemeManager.ActiveTheme> = MutableStateFlow(ThemeManager.currentColors(isInNightMode())) val BlackColorPalette = darkColors(
primary = Color(0xff0077e0), // If this value changes also need to update #0088ff in string resource files
primaryVariant = Color(0xff0077e0),
secondary = HighOrLowlight,
secondaryVariant = DarkGray,
background = Color(0xff070707),
surface = Color(0xff161617),
// background = Color(0xFF121212),
// surface = Color(0xFF121212),
error = Color.Red,
onBackground = Color(0xFFFFFBFA),
onSurface = Color(0xFFFFFBFA),
// onError: Color = Color.Black,
)
val BlackColorPaletteApp = AppColors(
title = Color(0xff0077e0),
primaryVariant2 = Color(0xff243747),
sentMessage = Color(0xFF18262E),
sentQuote = Color(0xFF1D3847),
receivedMessage = Color(0xff1b1b1b),
receivedQuote = Color(0xff29292b),
)
var systemInDarkThemeCurrently: Boolean = isInNightMode()
val CurrentColors: MutableStateFlow<ThemeManager.ActiveTheme> = MutableStateFlow(ThemeManager.currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get()))
@Composable @Composable
fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLight fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLight
@@ -263,31 +712,113 @@ fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLi
@Composable @Composable
expect fun isSystemInDarkTheme(): Boolean expect fun isSystemInDarkTheme(): Boolean
internal val LocalAppColors = staticCompositionLocalOf { LightColorPaletteApp }
internal val LocalAppWallpaper = staticCompositionLocalOf { AppWallpaper() }
val MaterialTheme.appColors: AppColors
@Composable
@ReadOnlyComposable
get() = LocalAppColors.current
fun AppColors.updateColorsFrom(other: AppColors) {
title = other.title
primaryVariant2 = other.primaryVariant2
sentMessage = other.sentMessage
sentQuote = other.sentQuote
receivedMessage = other.receivedMessage
receivedQuote = other.receivedQuote
}
fun AppWallpaper.updateWallpaperFrom(other: AppWallpaper) {
background = other.background
tint = other.tint
type = other.type
}
val MaterialTheme.wallpaper: AppWallpaper
@Composable
@ReadOnlyComposable
get() = LocalAppWallpaper.current
fun reactOnDarkThemeChanges(isDark: Boolean) { fun reactOnDarkThemeChanges(isDark: Boolean) {
if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM.name && CurrentColors.value.colors.isLight == isDark) { systemInDarkThemeCurrently = isDark
if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM_THEME_NAME && CurrentColors.value.colors.isLight == isDark) {
// Change active colors from light to dark and back based on system theme // Change active colors from light to dark and back based on system theme
ThemeManager.applyTheme(DefaultTheme.SYSTEM.name, isDark) ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
} }
} }
@Composable @Composable
fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) { fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
LaunchedEffect(darkTheme) { // TODO: Fix preview working with dark/light theme
// For preview
if (darkTheme != null) // LaunchedEffect(darkTheme) {
CurrentColors.value = ThemeManager.currentColors(darkTheme) // // For preview
} // if (darkTheme != null)
val systemDark = isSystemInDarkTheme() // CurrentColors.value = ThemeManager.currentColors(darkTheme, null, null, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())
LaunchedEffect(systemDark) { // }
reactOnDarkThemeChanges(systemDark) val systemDark = rememberUpdatedState(isSystemInDarkTheme())
LaunchedEffect(Unit) {
// snapshotFlow vs LaunchedEffect reduce number of recomposes
snapshotFlow { systemDark.value }
.collect {
reactOnDarkThemeChanges(systemDark.value)
}
} }
val theme by CurrentColors.collectAsState() val theme by CurrentColors.collectAsState()
LaunchedEffect(Unit) {
// snapshotFlow vs LaunchedEffect reduce number of recomposes when user is changed or it's themes
snapshotFlow { chatModel.currentUser.value?.uiThemes }
.collect {
ThemeManager.applyTheme(appPrefs.currentTheme.get()!!)
}
}
MaterialTheme( MaterialTheme(
colors = theme.colors, colors = theme.colors,
typography = Typography, typography = Typography,
shapes = Shapes, shapes = Shapes,
content = { content = {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colors.onBackground, content = content) val rememberedAppColors = remember {
// Explicitly creating a new object here so we don't mutate the initial [appColors]
// provided, and overwrite the values set in it.
theme.appColors.copy()
}.apply { updateColorsFrom(theme.appColors) }
val rememberedWallpaper = remember {
// Explicitly creating a new object here so we don't mutate the initial [wallpaper]
// provided, and overwrite the values set in it.
theme.wallpaper.copy()
}.apply { updateWallpaperFrom(theme.wallpaper) }
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colors.onBackground,
LocalAppColors provides rememberedAppColors,
LocalAppWallpaper provides rememberedWallpaper,
content = content)
}
)
}
@Composable
fun SimpleXThemeOverride(theme: ThemeManager.ActiveTheme, content: @Composable () -> Unit) {
MaterialTheme(
colors = theme.colors,
typography = Typography,
shapes = Shapes,
content = {
val rememberedAppColors = remember {
// Explicitly creating a new object here so we don't mutate the initial [appColors]
// provided, and overwrite the values set in it.
theme.appColors.copy()
}.apply { updateColorsFrom(theme.appColors) }
val rememberedWallpaper = remember {
// Explicitly creating a new object here so we don't mutate the initial [wallpaper]
// provided, and overwrite the values set in it.
theme.wallpaper.copy()
}.apply { updateWallpaperFrom(theme.wallpaper) }
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colors.onBackground,
LocalAppColors provides rememberedAppColors,
LocalAppWallpaper provides rememberedWallpaper,
content = content)
} }
) )
} }
@@ -1,14 +1,14 @@
package chat.simplex.common.ui.theme package chat.simplex.common.ui.theme
import androidx.compose.material.Colors import androidx.compose.material.Colors
import androidx.compose.runtime.MutableState
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import chat.simplex.res.MR import chat.simplex.common.model.*
import chat.simplex.common.model.AppPreferences import chat.simplex.common.platform.*
import chat.simplex.common.model.ChatController import chat.simplex.common.views.helpers.*
import chat.simplex.common.platform.platform import java.io.File
import chat.simplex.common.views.helpers.generalGetString
// https://github.com/rsms/inter // https://github.com/rsms/inter
// I place it here because IDEA shows an error (but still works anyway) when this declaration inside Type.kt // I place it here because IDEA shows an error (but still works anyway) when this declaration inside Type.kt
@@ -18,140 +18,215 @@ expect val EmojiFont: FontFamily
object ThemeManager { object ThemeManager {
private val appPrefs: AppPreferences = ChatController.appPrefs private val appPrefs: AppPreferences = ChatController.appPrefs
data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors) data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors, val wallpaper: AppWallpaper = AppWallpaper())
private fun systemDarkThemeColors(): Pair<Colors, DefaultTheme> = when (appPrefs.systemDarkTheme.get()) { private fun systemDarkThemeColors(): Pair<Colors, DefaultTheme> = when (appPrefs.systemDarkTheme.get()) {
DefaultTheme.DARK.name -> DarkColorPalette to DefaultTheme.DARK DefaultTheme.DARK.themeName -> DarkColorPalette to DefaultTheme.DARK
DefaultTheme.SIMPLEX.name -> SimplexColorPalette to DefaultTheme.SIMPLEX DefaultTheme.SIMPLEX.themeName -> SimplexColorPalette to DefaultTheme.SIMPLEX
DefaultTheme.BLACK.themeName -> BlackColorPalette to DefaultTheme.BLACK
else -> SimplexColorPalette to DefaultTheme.SIMPLEX else -> SimplexColorPalette to DefaultTheme.SIMPLEX
} }
fun currentColors(darkForSystemTheme: Boolean): ActiveTheme { private fun nonSystemThemeName(): String {
val themeName = appPrefs.currentTheme.get()!! val themeName = appPrefs.currentTheme.get()!!
val themeOverrides = appPrefs.themeOverrides.get() return if (themeName != DefaultTheme.SYSTEM_THEME_NAME) {
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) {
themeName themeName
} else { } else {
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name if (systemInDarkThemeCurrently) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.themeName
} }
val theme = themeOverrides[nonSystemThemeName] }
fun defaultActiveTheme(appSettingsTheme: List<ThemeOverrides>): ThemeOverrides? {
val nonSystemThemeName = nonSystemThemeName()
val defaultThemeId = appPrefs.currentThemeIds.get()[nonSystemThemeName]
return appSettingsTheme.getTheme(defaultThemeId)
}
fun defaultActiveTheme(perUserTheme: ThemeModeOverrides?, appSettingsTheme: List<ThemeOverrides>): ThemeModeOverride {
val perUserTheme = if (!CurrentColors.value.colors.isLight) perUserTheme?.dark else perUserTheme?.light
if (perUserTheme != null) {
return perUserTheme
}
val defaultTheme = defaultActiveTheme(appSettingsTheme)
return ThemeModeOverride(colors = defaultTheme?.colors ?: ThemeColors(), wallpaper = defaultTheme?.wallpaper)
}
fun currentColors(themeOverridesForType: WallpaperType?, perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverrides?, appSettingsTheme: List<ThemeOverrides>): ActiveTheme {
val themeName = appPrefs.currentTheme.get()!!
val nonSystemThemeName = nonSystemThemeName()
val defaultTheme = defaultActiveTheme(appSettingsTheme)
val baseTheme = when (nonSystemThemeName) { val baseTheme = when (nonSystemThemeName) {
DefaultTheme.LIGHT.name -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp) DefaultTheme.LIGHT.themeName -> ActiveTheme(DefaultTheme.LIGHT.themeName, DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.LIGHT)))
DefaultTheme.DARK.name -> Triple(DefaultTheme.DARK, DarkColorPalette, DarkColorPaletteApp) DefaultTheme.DARK.themeName -> ActiveTheme(DefaultTheme.DARK.themeName, DefaultTheme.DARK, DarkColorPalette, DarkColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.DARK)))
DefaultTheme.SIMPLEX.name -> Triple(DefaultTheme.SIMPLEX, SimplexColorPalette, SimplexColorPaletteApp) DefaultTheme.SIMPLEX.themeName -> ActiveTheme(DefaultTheme.SIMPLEX.themeName, DefaultTheme.SIMPLEX, SimplexColorPalette, SimplexColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.SIMPLEX)))
else -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp) DefaultTheme.BLACK.themeName -> ActiveTheme(DefaultTheme.BLACK.themeName, DefaultTheme.BLACK, BlackColorPalette, BlackColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.BLACK)))
else -> ActiveTheme(DefaultTheme.LIGHT.themeName, DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.LIGHT)))
} }
if (theme == null) {
return ActiveTheme(themeName, baseTheme.first, baseTheme.second, baseTheme.third) val perUserTheme = if (baseTheme.colors.isLight) perUserTheme?.light else perUserTheme?.dark
val theme = (appSettingsTheme.sameTheme(themeOverridesForType ?: perChatTheme?.type ?: perUserTheme?.type ?: defaultTheme?.wallpaper?.toAppWallpaper()?.type, nonSystemThemeName) ?: defaultTheme)
if (theme == null && perUserTheme == null && perChatTheme == null && themeOverridesForType == null) {
return ActiveTheme(themeName, baseTheme.base, baseTheme.colors, baseTheme.appColors, baseTheme.wallpaper)
} }
return ActiveTheme(themeName, baseTheme.first, theme.colors.toColors(theme.base), theme.colors.toAppColors(theme.base)) val presetWallpaperTheme = when {
perChatTheme?.wallpaper != null -> if (perChatTheme.wallpaper.preset != null) PresetWallpaper.from(perChatTheme.wallpaper.preset)?.colors?.get(baseTheme.base) else null
perUserTheme?.wallpaper != null -> if (perUserTheme.wallpaper.preset != null) PresetWallpaper.from(perUserTheme.wallpaper.preset)?.colors?.get(baseTheme.base) else null
else -> if (theme?.wallpaper?.preset != null) PresetWallpaper.from(theme.wallpaper.preset)?.colors?.get(baseTheme.base) else null
}
val themeOrEmpty = theme ?: ThemeOverrides(base = baseTheme.base)
val colors = themeOrEmpty.toColors(themeOrEmpty.base, perChatTheme?.colors, perUserTheme?.colors, presetWallpaperTheme)
return ActiveTheme(
themeName,
baseTheme.base,
colors,
themeOrEmpty.toAppColors(themeOrEmpty.base, perChatTheme?.colors, perChatTheme?.type, perUserTheme?.colors, perUserTheme?.type, presetWallpaperTheme),
themeOrEmpty.toAppWallpaper(themeOverridesForType, perChatTheme, perUserTheme, colors.background)
)
} }
fun currentThemeOverridesForExport(darkForSystemTheme: Boolean): ThemeOverrides { fun currentThemeOverridesForExport(perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverrides?): ThemeOverrides {
val themeName = appPrefs.currentTheme.get()!! val current = currentColors(null, perChatTheme, perUserTheme, appPrefs.themeOverrides.get())
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { val wType = current.wallpaper.type
themeName val wBackground = current.wallpaper.background
} else { val wTint = current.wallpaper.tint
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name return ThemeOverrides(
} themeId = "",
val overrides = appPrefs.themeOverrides.get().toMutableMap() base = current.base,
val nonFilledTheme = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) colors = ThemeColors.from(current.colors, current.appColors),
return nonFilledTheme.copy(colors = nonFilledTheme.colors.withFilledColors(CurrentColors.value.base)) wallpaper = if (wType !is WallpaperType.Empty) ThemeWallpaper.from(wType, wBackground?.toReadableHex(), wTint?.toReadableHex()).withFilledWallpaperBase64() else null
)
} }
// colors, default theme enum, localized name of theme fun applyTheme(theme: String) {
fun allThemes(darkForSystemTheme: Boolean): List<Triple<Colors, DefaultTheme, String>> {
val allThemes = ArrayList<Triple<Colors, DefaultTheme, String>>()
allThemes.add(
Triple(
if (darkForSystemTheme) systemDarkThemeColors().first else LightColorPalette,
DefaultTheme.SYSTEM,
generalGetString(MR.strings.theme_system)
)
)
allThemes.add(
Triple(
LightColorPalette,
DefaultTheme.LIGHT,
generalGetString(MR.strings.theme_light)
)
)
allThemes.add(
Triple(
DarkColorPalette,
DefaultTheme.DARK,
generalGetString(MR.strings.theme_dark)
)
)
allThemes.add(
Triple(
SimplexColorPalette,
DefaultTheme.SIMPLEX,
generalGetString(MR.strings.theme_simplex)
)
)
return allThemes
}
fun applyTheme(theme: String, darkForSystemTheme: Boolean) {
appPrefs.currentTheme.set(theme) appPrefs.currentTheme.set(theme)
CurrentColors.value = currentColors(darkForSystemTheme) CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
platform.androidSetNightModeIfSupported() platform.androidSetNightModeIfSupported()
} }
fun changeDarkTheme(theme: String, darkForSystemTheme: Boolean) { fun changeDarkTheme(theme: String) {
appPrefs.systemDarkTheme.set(theme) appPrefs.systemDarkTheme.set(theme)
CurrentColors.value = currentColors(darkForSystemTheme) CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
} }
fun saveAndApplyThemeColor(name: ThemeColor, color: Color? = null, darkForSystemTheme: Boolean) { fun saveAndApplyThemeColor(baseTheme: DefaultTheme, name: ThemeColor, color: Color? = null, pref: SharedPreference<List<ThemeOverrides>> = appPrefs.themeOverrides) {
val themeName = appPrefs.currentTheme.get()!! val nonSystemThemeName = baseTheme.themeName
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { val overrides = pref.get()
themeName val themeId = appPrefs.currentThemeIds.get()[nonSystemThemeName]
} else { val prevValue = overrides.getTheme(themeId) ?: ThemeOverrides(base = baseTheme)
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name pref.set(overrides.replace(prevValue.withUpdatedColor(name, color?.toReadableHex())))
val themeIds = appPrefs.currentThemeIds.get().toMutableMap()
themeIds[nonSystemThemeName] = prevValue.themeId
appPrefs.currentThemeIds.set(themeIds)
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
}
fun applyThemeColor(name: ThemeColor, color: Color? = null, pref: MutableState<ThemeModeOverride>) {
pref.value = pref.value.withUpdatedColor(name, color?.toReadableHex())
}
fun saveAndApplyWallpaper(baseTheme: DefaultTheme, type: WallpaperType?, pref: SharedPreference<List<ThemeOverrides>> = appPrefs.themeOverrides) {
val nonSystemThemeName = baseTheme.themeName
val overrides = pref.get()
val theme = overrides.sameTheme(type, baseTheme.themeName)
val prevValue = theme ?: ThemeOverrides(base = baseTheme)
pref.set(overrides.replace(prevValue.copy(wallpaper = if (type != null && type !is WallpaperType.Empty) ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint) else null)))
val themeIds = appPrefs.currentThemeIds.get().toMutableMap()
themeIds[nonSystemThemeName] = prevValue.themeId
appPrefs.currentThemeIds.set(themeIds)
CurrentColors.value = currentColors( null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
}
fun copyFromSameThemeOverrides(type: WallpaperType?, lowerLevelOverride: ThemeModeOverride?, pref: MutableState<ThemeModeOverride>): Boolean {
val overrides = appPrefs.themeOverrides.get()
val sameWallpaper = if (lowerLevelOverride?.type?.sameType(type) == true) lowerLevelOverride.wallpaper else overrides.sameTheme(type, CurrentColors.value.base.themeName)?.wallpaper
if (sameWallpaper == null) {
if (type != null) {
pref.value = ThemeModeOverride(wallpaper = ThemeWallpaper.from(type, null, null).copy(scale = null, scaleType = null))
} else {
// Make an empty wallpaper to override any top level ones
pref.value = ThemeModeOverride(wallpaper = ThemeWallpaper())
}
return true
} }
var colorToSet = color var type = sameWallpaper.toAppWallpaper().type
if (colorToSet == null) { if (type is WallpaperType.Image && sameWallpaper.imageFile == type.filename) {
// Setting default color from a base theme // 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
colorToSet = when(nonSystemThemeName) { val filename = saveWallpaperFile(File(getWallpaperFilePath(type.filename)).toURI())
DefaultTheme.LIGHT.name -> name.fromColors(LightColorPalette, LightColorPaletteApp) if (filename != null) {
DefaultTheme.DARK.name -> name.fromColors(DarkColorPalette, DarkColorPaletteApp) type = WallpaperType.Image(filename, type.scale, type.scaleType)
DefaultTheme.SIMPLEX.name -> name.fromColors(SimplexColorPalette, SimplexColorPaletteApp) } else {
// Will not be here Log.e(TAG, "Error while copying wallpaper from global overrides to chat overrides")
else -> return return false
} }
} }
val overrides = appPrefs.themeOverrides.get().toMutableMap() val prevValue = pref.value
val prevValue = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) pref.value = prevValue.copy(
overrides[nonSystemThemeName] = prevValue.withUpdatedColor(name, colorToSet.toReadableHex()) colors = ThemeColors(),
appPrefs.themeOverrides.set(overrides) wallpaper = ThemeWallpaper.from(type, null, null).copy(scale = null, scaleType = null)
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) )
return true
} }
fun saveAndApplyThemeOverrides(theme: ThemeOverrides, darkForSystemTheme: Boolean) { fun applyWallpaper(type: WallpaperType?, pref: MutableState<ThemeModeOverride>) {
val overrides = appPrefs.themeOverrides.get().toMutableMap() val prevValue = pref.value
val prevValue = overrides[theme.base.name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) pref.value = prevValue.copy(
overrides[theme.base.name] = prevValue.copy(colors = theme.colors) wallpaper = if (type != null)
appPrefs.themeOverrides.set(overrides) ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint)
appPrefs.currentTheme.set(theme.base.name) else null
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) )
} }
fun resetAllThemeColors(darkForSystemTheme: Boolean) { fun saveAndApplyThemeOverrides(theme: ThemeOverrides, pref: SharedPreference<List<ThemeOverrides>> = appPrefs.themeOverrides) {
val themeName = appPrefs.currentTheme.get()!! val wallpaper = theme.wallpaper?.importFromString()
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { val nonSystemThemeName = theme.base.themeName
themeName val overrides = pref.get()
} else { val prevValue = overrides.getTheme(null, wallpaper?.toAppWallpaper()?.type, theme.base) ?: ThemeOverrides(base = theme.base)
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name if (prevValue.wallpaper?.imageFile != null) {
File(getWallpaperFilePath(prevValue.wallpaper.imageFile)).delete()
}
pref.set(overrides.replace(prevValue.copy(base = theme.base, colors = theme.colors, wallpaper = wallpaper)))
appPrefs.currentTheme.set(nonSystemThemeName)
val currentThemeIds = appPrefs.currentThemeIds.get().toMutableMap()
currentThemeIds[nonSystemThemeName] = prevValue.themeId
appPrefs.currentThemeIds.set(currentThemeIds)
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
}
fun resetAllThemeColors(pref: SharedPreference<List<ThemeOverrides>> = appPrefs.themeOverrides) {
val nonSystemThemeName = nonSystemThemeName()
val themeId = appPrefs.currentThemeIds.get()[nonSystemThemeName] ?: return
val overrides = pref.get()
val prevValue = overrides.getTheme(themeId) ?: return
pref.set(overrides.replace(prevValue.copy(colors = ThemeColors(), wallpaper = prevValue.wallpaper?.copy(background = null, tint = null))))
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
}
fun resetAllThemeColors(pref: MutableState<ThemeModeOverride>) {
val prevValue = pref.value
pref.value = prevValue.copy(colors = ThemeColors(), wallpaper = prevValue.wallpaper?.copy(background = null, tint = null))
}
fun removeTheme(themeId: String?) {
val themes = ArrayList(appPrefs.themeOverrides.get())
themes.removeAll { it.themeId == themeId }
appPrefs.themeOverrides.set(themes)
}
fun String.colorFromReadableHex(): Color =
Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong())
fun Color.toReadableHex(): String {
val s = Integer.toHexString(toArgb())
return when {
this == Color.Transparent -> "#00ffffff"
s.length == 1 -> "#ff$s$s$s$s$s$s"
s.length == 2 -> "#ff$s$s$s"
s.length == 3 -> "#ff$s$s"
s.length == 6 && this.alpha == 0f -> "#00$s"
s.length == 6 -> "#ff$s"
else -> "#$s"
} }
val overrides = appPrefs.themeOverrides.get().toMutableMap()
val prevValue = overrides[nonSystemThemeName] ?: return
overrides[nonSystemThemeName] = prevValue.copy(colors = ThemeColors())
appPrefs.themeOverrides.set(overrides)
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
} }
} }
private fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb())
@@ -29,6 +29,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
@@ -41,6 +42,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import java.io.File
@Composable @Composable
fun ChatInfoView( fun ChatInfoView(
@@ -337,6 +339,16 @@ fun ChatInfoLayout(
if (cStats != null && cStats.ratchetSyncAllowed) { if (cStats != null && cStats.ratchetSyncAllowed) {
SynchronizeConnectionButton(syncContactConnection) SynchronizeConnectionButton(syncContactConnection)
} }
WallpaperButton {
ModalManager.end.showModal {
val chat = remember { derivedStateOf { chatModel.chats.firstOrNull { it.id == chat.id } } }
val c = chat.value
if (c != null) {
ChatWallpaperEditorModal(c)
}
}
}
// } else if (developerTools) { // } else if (developerTools) {
// SynchronizeConnectionButtonForce(syncContactConnectionForce) // SynchronizeConnectionButtonForce(syncContactConnectionForce)
// } // }
@@ -642,6 +654,15 @@ private fun SendReceiptsOption(currentUser: User, state: State<SendReceipts>, on
) )
} }
@Composable
fun WallpaperButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_image),
stringResource(MR.strings.settings_section_title_chat_theme),
click = onClick
)
}
@Composable @Composable
fun ClearChatButton(onClick: () -> Unit) { fun ClearChatButton(onClick: () -> Unit) {
SettingsActionItem( SettingsActionItem(
@@ -675,6 +696,72 @@ fun ShareAddressButton(onClick: () -> Unit) {
) )
} }
@Composable
fun ModalData.ChatWallpaperEditorModal(chat: Chat) {
val themes = remember(CurrentColors.collectAsState().value.base) {
(chat.chatInfo as? ChatInfo.Direct)?.contact?.uiThemes
?: (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.uiThemes
?: ThemeModeOverrides()
}
val globalThemeUsed = remember { stateGetOrPut("globalThemeUsed") { false } }
val initialTheme = remember(CurrentColors.collectAsState().value.base) {
val preferred = themes.preferredMode(!CurrentColors.value.colors.isLight)
globalThemeUsed.value = preferred == null
preferred ?: ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
}
ChatWallpaperEditor(
initialTheme,
applyToMode = if (themes.light == themes.dark) null else initialTheme.mode,
globalThemeUsed = globalThemeUsed,
save = { applyToMode, newTheme ->
save(applyToMode, newTheme, chatModel.getChat(chat.id) ?: chat)
})
}
suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, chat: Chat) {
val unchangedThemes: ThemeModeOverrides = ((chat.chatInfo as? ChatInfo.Direct)?.contact?.uiThemes ?: (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.uiThemes) ?: ThemeModeOverrides()
val wallpaperFiles = setOf(unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile)
var changedThemes: ThemeModeOverrides? = unchangedThemes
val changed = newTheme?.copy(wallpaper = newTheme.wallpaper?.withFilledWallpaperPath())
changedThemes = when (applyToMode) {
null -> changedThemes?.copy(light = changed?.copy(mode = DefaultThemeMode.LIGHT), dark = changed?.copy(mode = DefaultThemeMode.DARK))
DefaultThemeMode.LIGHT -> changedThemes?.copy(light = changed?.copy(mode = applyToMode))
DefaultThemeMode.DARK -> changedThemes?.copy(dark = changed?.copy(mode = applyToMode))
}
changedThemes = if (changedThemes?.light != null || changedThemes?.dark != null) {
val light = changedThemes.light
val dark = changedThemes.dark
val currentMode = CurrentColors.value.base.mode
// same image file for both modes, copy image to make them as different files
if (light?.wallpaper?.imageFile != null && dark?.wallpaper?.imageFile != null && light.wallpaper.imageFile == dark.wallpaper.imageFile) {
val imageFile = if (currentMode == DefaultThemeMode.LIGHT) {
dark.wallpaper.imageFile
} else {
light.wallpaper.imageFile
}
val filePath = saveWallpaperFile(File(getWallpaperFilePath(imageFile)).toURI())
changedThemes = if (currentMode == DefaultThemeMode.LIGHT) {
changedThemes.copy(dark = dark.copy(wallpaper = dark.wallpaper.copy(imageFile = filePath)))
} else {
changedThemes.copy(light = light.copy(wallpaper = light.wallpaper.copy(imageFile = filePath)))
}
}
changedThemes
} else {
null
}
val wallpaperFilesToDelete = wallpaperFiles - changedThemes?.light?.wallpaper?.imageFile - changedThemes?.dark?.wallpaper?.imageFile
wallpaperFilesToDelete.forEach(::removeWallpaperFile)
if (controller.apiSetChatUIThemes(chat.remoteHostId, chat.id, changedThemes)) {
if (chat.chatInfo is ChatInfo.Direct) {
chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes)))
} else if (chat.chatInfo is ChatInfo.Group) {
chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes)))
}
}
}
private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi { private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi {
val chatRh = chat.remoteHostId val chatRh = chat.remoteHostId
chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let { chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let {
@@ -42,7 +42,7 @@ sealed class CIInfoTab {
@Composable @Composable
fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) { fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
val sent = ci.chatDir.sent val sent = ci.chatDir.sent
val appColors = CurrentColors.collectAsState().value.appColors val appColors = MaterialTheme.appColors
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val selection = remember { mutableStateOf<CIInfoTab>(CIInfoTab.History) } val selection = remember { mutableStateOf<CIInfoTab>(CIInfoTab.History) }
@@ -5,25 +5,24 @@ import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.* import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.* import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.mapSaver import androidx.compose.runtime.saveable.mapSaver
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.* import androidx.compose.ui.*
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.text.* import androidx.compose.ui.text.*
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.* import chat.simplex.common.views.call.*
@@ -118,369 +117,377 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
when (chat.chatInfo) { when (chat.chatInfo) {
is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> { is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> {
ChatLayout( val perChatTheme = remember(chat.chatInfo, CurrentColors.value.base) { if (chat.chatInfo is ChatInfo.Direct) chat.chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chat.chatInfo is ChatInfo.Group) chat.chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null }
chat, val overrides = if (perChatTheme != null) ThemeManager.currentColors(null, perChatTheme, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) else null
unreadCount, SimpleXThemeOverride(overrides ?: CurrentColors.collectAsState().value) {
composeState, ChatLayout(
composeView = { chat,
Column( unreadCount,
Modifier.fillMaxWidth(), composeState,
horizontalAlignment = Alignment.CenterHorizontally composeView = {
) { Column(
if ( Modifier.fillMaxWidth(),
chat.chatInfo is ChatInfo.Direct horizontalAlignment = Alignment.CenterHorizontally
&& !chat.chatInfo.contact.ready
&& chat.chatInfo.contact.active
&& !chat.chatInfo.contact.nextSendGrpInv
) { ) {
Text( if (
generalGetString(MR.strings.contact_connection_pending), chat.chatInfo is ChatInfo.Direct
Modifier.padding(top = 4.dp), && !chat.chatInfo.contact.ready
fontSize = 14.sp, && chat.chatInfo.contact.active
color = MaterialTheme.colors.secondary && !chat.chatInfo.contact.nextSendGrpInv
) {
Text(
generalGetString(MR.strings.contact_connection_pending),
Modifier.padding(top = 4.dp),
fontSize = 14.sp,
color = MaterialTheme.colors.secondary
)
}
ComposeView(
chatModel, chat, composeState, attachmentOption,
showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }
) )
} }
ComposeView( },
chatModel, chat, composeState, attachmentOption, attachmentOption,
showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } } attachmentBottomSheetState,
) searchText,
} useLinkPreviews = useLinkPreviews,
}, linkMode = chatModel.simplexLinkMode.value,
attachmentOption, back = {
attachmentBottomSheetState, hideKeyboard(view)
searchText, AudioPlayer.stop()
useLinkPreviews = useLinkPreviews, chatModel.chatId.value = null
linkMode = chatModel.simplexLinkMode.value, chatModel.groupMembers.clear()
back = { },
hideKeyboard(view) info = {
AudioPlayer.stop() if (ModalManager.end.hasModalsOpen()) {
chatModel.chatId.value = null ModalManager.end.closeModals()
chatModel.groupMembers.clear() return@ChatLayout
},
info = {
if (ModalManager.end.hasModalsOpen()) {
ModalManager.end.closeModals()
return@ChatLayout
}
hideKeyboard(view)
withBGApi {
// The idea is to preload information before showing a modal because large groups can take time to load all members
var preloadedContactInfo: Pair<ConnectionStats?, Profile?>? = null
var preloadedCode: String? = null
var preloadedLink: Pair<String, GroupMemberRole>? = null
if (chat.chatInfo is ChatInfo.Direct) {
preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
} else if (chat.chatInfo is ChatInfo.Group) {
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
} }
ModalManager.end.showModalCloseable(true) { close -> hideKeyboard(view)
val chat = remember { activeChat }.value
if (chat?.chatInfo is ChatInfo.Direct) {
var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
var code: String? by remember { mutableStateOf(preloadedCode) }
KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) {
contactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
preloadedContactInfo = contactInfo
code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
preloadedCode = code
}
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
} else if (chat?.chatInfo is ChatInfo.Group) {
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
KeyChangeEffect(chat.id) {
setGroupMembers(chatRh, (chat.chatInfo as ChatInfo.Group).groupInfo, chatModel)
link = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
preloadedLink = link
}
GroupChatInfoView(chatModel, chatRh, chat.id, link?.first, link?.second, {
link = it
preloadedLink = it
}, close)
}
}
}
},
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
hideKeyboard(view)
withBGApi {
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
val stats = r?.second
val (_, code) = if (member.memberActive) {
val memCode = chatModel.controller.apiGetGroupMemberCode(chatRh, groupInfo.apiId, member.groupMemberId)
member to memCode?.second
} else {
member to null
}
setGroupMembers(chatRh, groupInfo, chatModel)
ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(true) { close ->
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close)
}
}
}
},
loadPrevMessages = {
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout)
val firstId = chatModel.chatItems.value.firstOrNull()?.id
if (c != null && firstId != null) {
withBGApi { withBGApi {
apiLoadPrevMessages(c, chatModel, firstId, searchText.value) // The idea is to preload information before showing a modal because large groups can take time to load all members
var preloadedContactInfo: Pair<ConnectionStats?, Profile?>? = null
var preloadedCode: String? = null
var preloadedLink: Pair<String, GroupMemberRole>? = null
if (chat.chatInfo is ChatInfo.Direct) {
preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
} else if (chat.chatInfo is ChatInfo.Group) {
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
}
ModalManager.end.showModalCloseable(true) { close ->
val chat = remember { activeChat }.value
if (chat?.chatInfo is ChatInfo.Direct) {
var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
var code: String? by remember { mutableStateOf(preloadedCode) }
KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) {
contactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
preloadedContactInfo = contactInfo
code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
preloadedCode = code
}
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
} else if (chat?.chatInfo is ChatInfo.Group) {
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
KeyChangeEffect(chat.id) {
setGroupMembers(chatRh, (chat.chatInfo as ChatInfo.Group).groupInfo, chatModel)
link = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
preloadedLink = link
}
GroupChatInfoView(chatModel, chatRh, chat.id, link?.first, link?.second, {
link = it
preloadedLink = it
}, close)
}
}
} }
} },
}, showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
deleteMessage = { itemId, mode -> hideKeyboard(view)
withBGApi { withBGApi {
val cInfo = chat.chatInfo val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId } val stats = r?.second
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo) val (_, code) = if (member.memberActive) {
val groupInfo = toModerate?.first val memCode = chatModel.controller.apiGetGroupMemberCode(chatRh, groupInfo.apiId, member.groupMemberId)
val groupMember = toModerate?.second member to memCode?.second
val deletedChatItem: ChatItem? } else {
val toChatItem: ChatItem? member to null
if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) { }
val r = chatModel.controller.apiDeleteMemberChatItem( setGroupMembers(chatRh, groupInfo, chatModel)
chatRh, ModalManager.end.closeModals()
groupId = groupInfo.groupId, ModalManager.end.showModalCloseable(true) { close ->
groupMemberId = groupMember.groupMemberId, remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
itemId = itemId GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close)
) }
deletedChatItem = r?.first }
toChatItem = r?.second }
} else { },
val r = chatModel.controller.apiDeleteChatItem( loadPrevMessages = {
chatRh, if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout)
val firstId = chatModel.chatItems.value.firstOrNull()?.id
if (c != null && firstId != null) {
withBGApi {
apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
}
}
},
deleteMessage = { itemId, mode ->
withBGApi {
val cInfo = chat.chatInfo
val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId }
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
val groupInfo = toModerate?.first
val groupMember = toModerate?.second
val deletedChatItem: ChatItem?
val toChatItem: ChatItem?
if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) {
val r = chatModel.controller.apiDeleteMemberChatItem(
chatRh,
groupId = groupInfo.groupId,
groupMemberId = groupMember.groupMemberId,
itemId = itemId
)
deletedChatItem = r?.first
toChatItem = r?.second
} else {
val r = chatModel.controller.apiDeleteChatItem(
chatRh,
type = cInfo.chatType,
id = cInfo.apiId,
itemId = itemId,
mode = mode
)
deletedChatItem = r?.deletedChatItem?.chatItem
toChatItem = r?.toChatItem?.chatItem
}
if (toChatItem == null && deletedChatItem != null) {
chatModel.removeChatItem(chatRh, cInfo, deletedChatItem)
} else if (toChatItem != null) {
chatModel.upsertChatItem(chatRh, cInfo, toChatItem)
}
}
},
deleteMessages = { itemIds ->
if (itemIds.isNotEmpty()) {
val chatInfo = chat.chatInfo
withBGApi {
val deletedItems: ArrayList<ChatItem> = arrayListOf()
for (itemId in itemIds) {
val di = chatModel.controller.apiDeleteChatItem(
chatRh, chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal
)?.deletedChatItem?.chatItem
if (di != null) {
deletedItems.add(di)
}
}
for (di in deletedItems) {
chatModel.removeChatItem(chatRh, chatInfo, di)
}
}
}
},
receiveFile = { fileId ->
withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId) }
},
cancelFile = { fileId ->
withBGApi { chatModel.controller.cancelFile(chatRh, user, fileId) }
},
joinGroup = { groupId, onComplete ->
withBGApi {
chatModel.controller.apiJoinGroup(chatRh, groupId)
onComplete.invoke()
}
},
startCall = out@{ media ->
withBGApi {
val cInfo = chat.chatInfo
if (cInfo is ChatInfo.Direct) {
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId)
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
chatModel.showCallView.value = true
chatModel.callCommand.add(WCallCommand.Capabilities(media))
}
}
},
endCall = {
val call = chatModel.activeCall.value
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
},
acceptCall = { contact ->
hideKeyboard(view)
withBGApi {
val invitation = chatModel.callInvitations.remove(contact.id)
?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id }
if (invitation == null) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
} else {
chatModel.callManager.acceptIncomingCall(invitation = invitation)
}
}
},
acceptFeature = { contact, feature, param ->
withBGApi {
chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param)
}
},
openDirectChat = { contactId ->
withBGApi {
openDirectChat(chatRh, contactId, chatModel)
}
},
forwardItem = { cItem, cInfo ->
chatModel.chatId.value = null
chatModel.sharedContent.value = SharedContent.Forward(cInfo, cItem)
},
updateContactStats = { contact ->
withBGApi {
val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
if (r != null) {
val contactStats = r.first
if (contactStats != null)
chatModel.updateContactConnectionStats(chatRh, contact, contactStats)
}
}
},
updateMemberStats = { groupInfo, member ->
withBGApi {
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
if (r != null) {
val memStats = r.second
if (memStats != null) {
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats)
}
}
}
},
syncContactConnection = { contact ->
withBGApi {
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
if (cStats != null) {
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
}
}
},
syncMemberConnection = { groupInfo, member ->
withBGApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) {
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second)
}
}
},
findModelChat = { chatId ->
chatModel.getChat(chatId)
},
findModelMember = { memberId ->
chatModel.groupMembers.find { it.id == memberId }
},
setReaction = { cInfo, cItem, add, reaction ->
withBGApi {
val updatedCI = chatModel.controller.apiChatItemReaction(
rh = chatRh,
type = cInfo.chatType, type = cInfo.chatType,
id = cInfo.apiId, id = cInfo.apiId,
itemId = itemId, itemId = cItem.id,
mode = mode add = add,
reaction = reaction
) )
deletedChatItem = r?.deletedChatItem?.chatItem if (updatedCI != null) {
toChatItem = r?.toChatItem?.chatItem chatModel.updateChatItem(cInfo, updatedCI)
}
} }
if (toChatItem == null && deletedChatItem != null) { },
chatModel.removeChatItem(chatRh, cInfo, deletedChatItem) showItemDetails = { cInfo, cItem ->
} else if (toChatItem != null) { suspend fun loadChatItemInfo(): ChatItemInfo? {
chatModel.upsertChatItem(chatRh, cInfo, toChatItem) val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id)
} if (ciInfo != null) {
} if (chat.chatInfo is ChatInfo.Group) {
}, setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
deleteMessages = { itemIds ->
if (itemIds.isNotEmpty()) {
val chatInfo = chat.chatInfo
withBGApi {
val deletedItems: ArrayList<ChatItem> = arrayListOf()
for (itemId in itemIds) {
val di = chatModel.controller.apiDeleteChatItem(
chatRh, chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal
)?.deletedChatItem?.chatItem
if (di != null) {
deletedItems.add(di)
} }
} }
for (di in deletedItems) { return ciInfo
chatModel.removeChatItem(chatRh, chatInfo, di)
}
} }
} withBGApi {
}, var initialCiInfo = loadChatItemInfo() ?: return@withBGApi
receiveFile = { fileId -> ModalManager.end.closeModals()
withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId) } ModalManager.end.showModalCloseable(endButtons = {
}, ShareButton {
cancelFile = { fileId -> clipboard.shareText(itemInfoShareText(chatModel, cItem, initialCiInfo, chatModel.controller.appPrefs.developerTools.get()))
withBGApi { chatModel.controller.cancelFile(chatRh, user, fileId) } }
}, }) { close ->
joinGroup = { groupId, onComplete -> var ciInfo by remember(cItem.id) { mutableStateOf(initialCiInfo) }
withBGApi { ChatItemInfoView(chatRh, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
chatModel.controller.apiJoinGroup(chatRh, groupId) LaunchedEffect(cItem.id) {
onComplete.invoke() withContext(Dispatchers.Default) {
} for (apiResp in controller.messagesChannel) {
}, val msg = apiResp.resp
startCall = out@{ media -> if (apiResp.remoteHostId == chatRh &&
withBGApi { msg is CR.ChatItemStatusUpdated &&
val cInfo = chat.chatInfo msg.chatItem.chatItem.id == cItem.id
if (cInfo is ChatInfo.Direct) { ) {
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId) ciInfo = loadChatItemInfo() ?: return@withContext
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi initialCiInfo = ciInfo
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile) }
chatModel.showCallView.value = true
chatModel.callCommand.add(WCallCommand.Capabilities(media))
}
}
},
endCall = {
val call = chatModel.activeCall.value
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
},
acceptCall = { contact ->
hideKeyboard(view)
withBGApi {
val invitation = chatModel.callInvitations.remove(contact.id)
?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id }
if (invitation == null) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
} else {
chatModel.callManager.acceptIncomingCall(invitation = invitation)
}
}
},
acceptFeature = { contact, feature, param ->
withBGApi {
chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param)
}
},
openDirectChat = { contactId ->
withBGApi {
openDirectChat(chatRh, contactId, chatModel)
}
},
updateContactStats = { contact ->
withBGApi {
val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
if (r != null) {
val contactStats = r.first
if (contactStats != null)
chatModel.updateContactConnectionStats(chatRh, contact, contactStats)
}
}
},
updateMemberStats = { groupInfo, member ->
withBGApi {
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
if (r != null) {
val memStats = r.second
if (memStats != null) {
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats)
}
}
}
},
syncContactConnection = { contact ->
withBGApi {
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
if (cStats != null) {
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
}
}
},
syncMemberConnection = { groupInfo, member ->
withBGApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) {
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second)
}
}
},
findModelChat = { chatId ->
chatModel.getChat(chatId)
},
findModelMember = { memberId ->
chatModel.groupMembers.find { it.id == memberId }
},
setReaction = { cInfo, cItem, add, reaction ->
withBGApi {
val updatedCI = chatModel.controller.apiChatItemReaction(
rh = chatRh,
type = cInfo.chatType,
id = cInfo.apiId,
itemId = cItem.id,
add = add,
reaction = reaction
)
if (updatedCI != null) {
chatModel.updateChatItem(cInfo, updatedCI)
}
}
},
showItemDetails = { cInfo, cItem ->
suspend fun loadChatItemInfo(): ChatItemInfo? {
val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id)
if (ciInfo != null) {
if (chat.chatInfo is ChatInfo.Group) {
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
}
}
return ciInfo
}
withBGApi {
var initialCiInfo = loadChatItemInfo() ?: return@withBGApi
ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(endButtons = {
ShareButton {
clipboard.shareText(itemInfoShareText(chatModel, cItem, initialCiInfo, chatModel.controller.appPrefs.developerTools.get()))
}
}) { close ->
var ciInfo by remember(cItem.id) { mutableStateOf(initialCiInfo) }
ChatItemInfoView(chatRh, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
LaunchedEffect(cItem.id) {
withContext(Dispatchers.Default) {
for (apiResp in controller.messagesChannel) {
val msg = apiResp.resp
if (apiResp.remoteHostId == chatRh &&
msg is CR.ChatItemStatusUpdated &&
msg.chatItem.chatItem.id == cItem.id
) {
ciInfo = loadChatItemInfo() ?: return@withContext
initialCiInfo = ciInfo
} }
} }
} }
} KeyChangeEffect(chatModel.chatId.value) {
KeyChangeEffect(chatModel.chatId.value) { close()
close() }
} }
} }
} },
}, addMembers = { groupInfo ->
addMembers = { groupInfo -> hideKeyboard(view)
hideKeyboard(view) withBGApi {
withBGApi { setGroupMembers(chatRh, groupInfo, chatModel)
setGroupMembers(chatRh, groupInfo, chatModel) ModalManager.end.closeModals()
ModalManager.end.closeModals() ModalManager.end.showModalCloseable(true) { close ->
ModalManager.end.showModalCloseable(true) { close -> AddGroupMembersView(chatRh, groupInfo, false, chatModel, close)
AddGroupMembersView(chatRh, groupInfo, false, chatModel, close) }
} }
} },
}, openGroupLink = { groupInfo ->
openGroupLink = { groupInfo -> hideKeyboard(view)
hideKeyboard(view) withBGApi {
withBGApi { val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId)
val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId) ModalManager.end.closeModals()
ModalManager.end.closeModals() ModalManager.end.showModalCloseable(true) {
ModalManager.end.showModalCloseable(true) { GroupLinkView(chatModel, chatRh, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null)
GroupLinkView(chatModel, chatRh, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null) }
} }
} },
}, markRead = { range, unreadCountAfter ->
markRead = { range, unreadCountAfter -> chatModel.markChatItemsRead(chat, range, unreadCountAfter)
chatModel.markChatItemsRead(chat, range, unreadCountAfter) ntfManager.cancelNotificationsForChat(chat.id)
ntfManager.cancelNotificationsForChat(chat.id) withBGApi {
withBGApi { chatModel.controller.apiChatRead(
chatModel.controller.apiChatRead( chatRh,
chatRh, chat.chatInfo.chatType,
chat.chatInfo.chatType, chat.chatInfo.apiId,
chat.chatInfo.apiId, range
range )
) }
} },
}, changeNtfsState = { enabled, currentValue -> toggleNotifications(chat, enabled, chatModel, currentValue) },
changeNtfsState = { enabled, currentValue -> toggleNotifications(chat, enabled, chatModel, currentValue) }, onSearchValueChanged = { value ->
onSearchValueChanged = { value -> if (searchText.value == value) return@ChatLayout
if (searchText.value == value) return@ChatLayout if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) ?: return@ChatLayout
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) ?: return@ChatLayout withBGApi {
withBGApi { apiFindMessages(c, chatModel, value)
apiFindMessages(c, chatModel, value) searchText.value = value
searchText.value = value }
} },
}, onComposed,
onComposed, developerTools = chatModel.controller.appPrefs.developerTools.get(),
developerTools = chatModel.controller.appPrefs.developerTools.get(), showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(),
showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(), )
) }
} }
is ChatInfo.ContactConnection -> { is ChatInfo.ContactConnection -> {
val close = { chatModel.chatId.value = null } val close = { chatModel.chatId.value = null }
@@ -534,6 +541,7 @@ fun ChatLayout(
acceptCall: (Contact) -> Unit, acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
openDirectChat: (Long) -> Unit, openDirectChat: (Long) -> Unit,
forwardItem: (ChatInfo, ChatItem) -> Unit,
updateContactStats: (Contact) -> Unit, updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit, updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> Unit, syncContactConnection: (Contact) -> Unit,
@@ -598,15 +606,25 @@ fun ChatLayout(
floatingActionButton = { floatingButton.value() }, floatingActionButton = { floatingButton.value() },
contentColor = LocalContentColor.current, contentColor = LocalContentColor.current,
drawerContentColor = LocalContentColor.current, drawerContentColor = LocalContentColor.current,
backgroundColor = Color.Unspecified
) { contentPadding -> ) { contentPadding ->
val wallpaperImage = MaterialTheme.wallpaper.type.image
val wallpaperType = MaterialTheme.wallpaper.type
val backgroundColor = MaterialTheme.wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, MaterialTheme.colors.background)
val tintColor = MaterialTheme.wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base)
BoxWithConstraints(Modifier BoxWithConstraints(Modifier
.fillMaxHeight() .fillMaxSize()
.background(MaterialTheme.colors.background)
.then(if (wallpaperImage != null)
Modifier.drawBehind { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) }
else
Modifier)
.padding(contentPadding) .padding(contentPadding)
) { ) {
ChatItemsList( ChatItemsList(
chat, unreadCount, composeState, searchValue, chat, unreadCount, composeState, searchValue,
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages, useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy, setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy,
) )
@@ -875,6 +893,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
acceptCall: (Contact) -> Unit, acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
openDirectChat: (Long) -> Unit, openDirectChat: (Long) -> Unit,
forwardItem: (ChatInfo, ChatItem) -> Unit,
updateContactStats: (Contact) -> Unit, updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit, updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> Unit, syncContactConnection: (Contact) -> Unit,
@@ -978,7 +997,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
tryOrShowError("${cItem.id}ChatItem", error = { tryOrShowError("${cItem.id}ChatItem", error = {
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
}) { }) {
ChatItemView(chat.remoteHostId, chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy) ChatItemView(chat.remoteHostId, chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy)
} }
} }
@@ -1262,7 +1281,7 @@ val MEMBER_IMAGE_SIZE: Dp = 38.dp
@Composable @Composable
fun MemberImage(member: GroupMember) { fun MemberImage(member: GroupMember) {
ProfileImage(MEMBER_IMAGE_SIZE, member.memberProfile.image) ProfileImage(MEMBER_IMAGE_SIZE, member.memberProfile.image, backgroundColor = MaterialTheme.colors.background)
} }
@Composable @Composable
@@ -1531,6 +1550,7 @@ fun PreviewChatLayout() {
acceptCall = { _ -> }, acceptCall = { _ -> },
acceptFeature = { _, _, _ -> }, acceptFeature = { _, _, _ -> },
openDirectChat = { _ -> }, openDirectChat = { _ -> },
forwardItem = { _, _ -> },
updateContactStats = { }, updateContactStats = { },
updateMemberStats = { _, _ -> }, updateMemberStats = { _, _ -> },
syncContactConnection = { }, syncContactConnection = { },
@@ -1604,6 +1624,7 @@ fun PreviewGroupChatLayout() {
acceptCall = { _ -> }, acceptCall = { _ -> },
acceptFeature = { _, _, _ -> }, acceptFeature = { _, _, _ -> },
openDirectChat = { _ -> }, openDirectChat = { _ -> },
forwardItem = { _, _ -> },
updateContactStats = { }, updateContactStats = { },
updateMemberStats = { _, _ -> }, updateMemberStats = { _, _ -> },
syncContactConnection = { }, syncContactConnection = { },
@@ -16,7 +16,7 @@ import dev.icerock.moko.resources.compose.stringResource
@Composable @Composable
fun ComposeContextInvitingContactMemberView() { fun ComposeContextInvitingContactMemberView() {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val sentColor = MaterialTheme.appColors.sentMessage
Row( Row(
Modifier Modifier
.height(60.dp) .height(60.dp)
@@ -16,7 +16,7 @@ import chat.simplex.res.MR
@Composable @Composable
fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boolean) { fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boolean) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val sentColor = MaterialTheme.appColors.sentMessage
Row( Row(
Modifier Modifier
.height(60.dp) .height(60.dp)
@@ -20,7 +20,7 @@ import chat.simplex.common.views.helpers.UploadContent
@Composable @Composable
fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) { fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val sentColor = MaterialTheme.appColors.sentMessage
Row( Row(
Modifier Modifier
.padding(top = 8.dp) .padding(top = 8.dp)
@@ -776,7 +776,7 @@ fun ComposeView(
@Composable @Composable
fun MsgNotAllowedView(reason: String, icon: Painter) { fun MsgNotAllowedView(reason: String, icon: Painter) {
val color = CurrentColors.collectAsState().value.appColors.receivedMessage val color = MaterialTheme.appColors.receivedMessage
Row(Modifier.padding(top = 5.dp).fillMaxWidth().background(color).padding(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF * 1.5f), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.padding(top = 5.dp).fillMaxWidth().background(color).padding(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF * 1.5f), verticalAlignment = Alignment.CenterVertically) {
Icon(icon, null, tint = MaterialTheme.colors.secondary) Icon(icon, null, tint = MaterialTheme.colors.secondary)
Spacer(Modifier.width(DEFAULT_PADDING_HALF)) Spacer(Modifier.width(DEFAULT_PADDING_HALF))
@@ -862,7 +862,7 @@ fun ComposeView(
} }
} }
Row( Row(
modifier = Modifier.padding(end = 8.dp), modifier = Modifier.background(MaterialTheme.colors.background).padding(end = 8.dp),
verticalAlignment = Alignment.Bottom, verticalAlignment = Alignment.Bottom,
) { ) {
val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership) val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership)
@@ -974,7 +974,7 @@ fun ComposeView(
val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) } val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) }
val sendButtonColor = val sendButtonColor =
if (chat.chatInfo.incognito) if (chat.chatInfo.incognito)
if (isSystemInDarkTheme()) Indigo else Indigo.copy(alpha = 0.7F) if (isInDarkTheme()) Indigo else Indigo.copy(alpha = 0.7F)
else MaterialTheme.colors.primary else MaterialTheme.colors.primary
SendMsgView( SendMsgView(
composeState, composeState,
@@ -35,7 +35,7 @@ fun ComposeVoiceView(
) { ) {
val progress = rememberSaveable { mutableStateOf(0) } val progress = rememberSaveable { mutableStateOf(0) }
val duration = rememberSaveable(recordedDurationMs) { mutableStateOf(recordedDurationMs) } val duration = rememberSaveable(recordedDurationMs) { mutableStateOf(recordedDurationMs) }
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val sentColor = MaterialTheme.appColors.sentMessage
Box { Box {
Box( Box(
Modifier Modifier
@@ -30,8 +30,8 @@ fun ContextItemView(
cancelContextItem: () -> Unit cancelContextItem: () -> Unit
) { ) {
val sent = contextItem.chatDir.sent val sent = contextItem.chatDir.sent
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val sentColor = MaterialTheme.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage val receivedColor = MaterialTheme.appColors.receivedMessage
@Composable @Composable
fun MessageText(attachment: ImageResource?, lines: Int) { fun MessageText(attachment: ImageResource?, lines: Int) {
@@ -9,7 +9,6 @@ import SectionSpacer
import SectionTextFooter import SectionTextFooter
import SectionView import SectionView
import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.* import androidx.compose.foundation.lazy.*
import androidx.compose.material.* import androidx.compose.material.*
@@ -233,6 +232,16 @@ fun GroupChatInfoLayout(
} else { } else {
SendReceiptsOptionDisabled() SendReceiptsOptionDisabled()
} }
WallpaperButton {
ModalManager.end.showModal {
val chat = remember { derivedStateOf { chatModel.chats.firstOrNull { it.id == chat.id } } }
val c = chat.value
if (c != null) {
ChatWallpaperEditorModal(c)
}
}
}
} }
SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs)) SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs))
SectionDividerSpaced(maxTopPadding = true) SectionDividerSpaced(maxTopPadding = true)
@@ -123,13 +123,12 @@ private fun GroupPreferencesLayout(
applyPrefs(preferences.copy(files = RoleGroupPreference(enable = enable, role))) applyPrefs(preferences.copy(files = RoleGroupPreference(enable = enable, role)))
} }
// TODO enable simplexLinks preference in 5.8 SectionDividerSpaced(true, maxBottomPadding = false)
// SectionDividerSpaced(true, maxBottomPadding = false) val allowSimplexLinks = remember(preferences) { mutableStateOf(preferences.simplexLinks.enable) }
// val allowSimplexLinks = remember(preferences) { mutableStateOf(preferences.simplexLinks.enable) } val simplexLinksRole = remember(preferences) { mutableStateOf(preferences.simplexLinks.role) }
// val simplexLinksRole = remember(preferences) { mutableStateOf(preferences.simplexLinks.role) } FeatureSection(GroupFeature.SimplexLinks, allowSimplexLinks, simplexLinksRole, groupInfo, preferences, onTTLUpdated) { enable, role ->
// FeatureSection(GroupFeature.SimplexLinks, allowSimplexLinks, simplexLinksRole, groupInfo, preferences, onTTLUpdated) { enable, role -> applyPrefs(preferences.copy(simplexLinks = RoleGroupPreference(enable = enable, role)))
// applyPrefs(preferences.copy(simplexLinks = RoleGroupPreference(enable = enable, role))) }
// }
SectionDividerSpaced(true, maxBottomPadding = false) SectionDividerSpaced(true, maxBottomPadding = false)
val enableHistory = remember(preferences) { mutableStateOf(preferences.history.enable) } val enableHistory = remember(preferences) { mutableStateOf(preferences.history.enable) }
@@ -189,8 +188,6 @@ private fun FeatureSection(
generalGetString(MR.strings.feature_enabled_for), generalGetString(MR.strings.feature_enabled_for),
featureRoles, featureRoles,
enableForRole, enableForRole,
// remove in v5.8
enabled = remember { mutableStateOf(false) },
onSelected = { value -> onSelected = { value ->
onSelected(enableFeature.value, value) onSelected(enableFeature.value, value)
} }
@@ -83,8 +83,8 @@ fun CIGroupInvitationView(
} }
} }
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val sentColor = MaterialTheme.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage val receivedColor = MaterialTheme.appColors.receivedMessage
Surface( Surface(
modifier = if (action && !inProgress.value) Modifier.clickable(onClick = { modifier = if (action && !inProgress.value) Modifier.clickable(onClick = {
inProgress.value = true inProgress.value = true
@@ -110,6 +110,7 @@ fun CIGroupInvitationView(
.padding(bottom = 4.dp), .padding(bottom = 4.dp),
) { ) {
groupInfoView() groupInfoView()
val secondaryColor = MaterialTheme.colors.secondary
Column(Modifier.padding(top = 2.dp, start = 5.dp)) { Column(Modifier.padding(top = 2.dp, start = 5.dp)) {
Divider(Modifier.fillMaxWidth().padding(bottom = 4.dp)) Divider(Modifier.fillMaxWidth().padding(bottom = 4.dp))
if (action) { if (action) {
@@ -117,7 +118,7 @@ fun CIGroupInvitationView(
Text( Text(
buildAnnotatedString { buildAnnotatedString {
append(generalGetString(if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join)) append(generalGetString(if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join))
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false)) } withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor)) }
}, },
color = if (inProgress.value) color = if (inProgress.value)
MaterialTheme.colors.secondary MaterialTheme.colors.secondary
@@ -128,7 +129,7 @@ fun CIGroupInvitationView(
Text( Text(
buildAnnotatedString { buildAnnotatedString {
append(groupInvitationStr()) append(groupInvitationStr())
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false)) } withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor)) }
} }
) )
} }
@@ -12,7 +12,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.isInDarkTheme import chat.simplex.common.ui.theme.isInDarkTheme
import chat.simplex.res.MR import chat.simplex.res.MR
@@ -86,7 +85,7 @@ private fun CIMetaText(
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
} }
if (showViaProxy && meta.sentViaProxy == true) { if (showViaProxy && meta.sentViaProxy == true) {
Icon(painterResource(MR.images.ic_arrow_forward), null, Modifier.height(17.dp), tint = CurrentColors.value.colors.secondary) Icon(painterResource(MR.images.ic_arrow_forward), null, Modifier.height(17.dp), tint = MaterialTheme.colors.secondary)
} }
if (showStatus) { if (showStatus) {
val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color, paleColor) val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color, paleColor)
@@ -115,6 +114,7 @@ fun reserveSpaceForMeta(
meta: CIMeta, meta: CIMeta,
chatTTL: Int?, chatTTL: Int?,
encrypted: Boolean?, encrypted: Boolean?,
secondaryColor: Color,
showStatus: Boolean = true, showStatus: Boolean = true,
showEdited: Boolean = true, showEdited: Boolean = true,
showViaProxy: Boolean = false showViaProxy: Boolean = false
@@ -132,7 +132,7 @@ fun reserveSpaceForMeta(
if (showViaProxy && meta.sentViaProxy == true) { if (showViaProxy && meta.sentViaProxy == true) {
res += iconSpace res += iconSpace
} }
if (showStatus && (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing)) { if (showStatus && (meta.statusIcon(secondaryColor) != null || !meta.disappearing)) {
res += iconSpace res += iconSpace
} }
if (encrypted != null) { if (encrypted != null) {
@@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.common.ui.theme.appColors
import chat.simplex.common.views.helpers.AlertManager import chat.simplex.common.views.helpers.AlertManager
import chat.simplex.common.views.helpers.generalGetString import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR import chat.simplex.res.MR
@@ -137,7 +138,7 @@ fun DecryptionErrorItemFixButton(
onClick: () -> Unit, onClick: () -> Unit,
syncSupported: Boolean syncSupported: Boolean
) { ) {
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage val receivedColor = MaterialTheme.appColors.receivedMessage
Surface( Surface(
Modifier.clickable(onClick = onClick), Modifier.clickable(onClick = onClick),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
@@ -164,10 +165,11 @@ fun DecryptionErrorItemFixButton(
tint = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary tint = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
) )
Spacer(Modifier.padding(2.dp)) Spacer(Modifier.padding(2.dp))
val secondaryColor = MaterialTheme.colors.secondary
Text( Text(
buildAnnotatedString { buildAnnotatedString {
append(generalGetString(MR.strings.fix_connection)) append(generalGetString(MR.strings.fix_connection))
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null)) } withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor)) }
withStyle(reserveTimestampStyle) { append(" ") } // for icon withStyle(reserveTimestampStyle) { append(" ") } // for icon
}, },
color = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary color = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
@@ -184,7 +186,7 @@ fun DecryptionErrorItem(
ci: ChatItem, ci: ChatItem,
onClick: () -> Unit onClick: () -> Unit
) { ) {
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage val receivedColor = MaterialTheme.appColors.receivedMessage
Surface( Surface(
Modifier.clickable(onClick = onClick), Modifier.clickable(onClick = onClick),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
@@ -195,10 +197,11 @@ fun DecryptionErrorItem(
Modifier.padding(vertical = 6.dp, horizontal = 12.dp), Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
contentAlignment = Alignment.BottomEnd, contentAlignment = Alignment.BottomEnd,
) { ) {
val secondaryColor = MaterialTheme.colors.secondary
Text( Text(
buildAnnotatedString { buildAnnotatedString {
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) } withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) }
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null)) } withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor)) }
}, },
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp) style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)
) )
@@ -154,8 +154,8 @@ private fun VoiceLayout(
} }
when { when {
hasText -> { hasText -> {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val sentColor = MaterialTheme.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage val receivedColor = MaterialTheme.appColors.receivedMessage
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile) VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile)
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@@ -224,8 +224,8 @@ private fun PlayPauseButton(
longClick: () -> Unit, longClick: () -> Unit,
icon: ImageResource = MR.images.ic_play_arrow_filled, icon: ImageResource = MR.images.ic_play_arrow_filled,
) { ) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val sentColor = MaterialTheme.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage val receivedColor = MaterialTheme.appColors.receivedMessage
Surface( Surface(
Modifier.drawRingModifier(angle, strokeColor, strokeWidth), Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
color = if (sent) sentColor else receivedColor, color = if (sent) sentColor else receivedColor,
@@ -59,6 +59,7 @@ fun ChatItemView(
scrollToItem: (Long) -> Unit, scrollToItem: (Long) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
openDirectChat: (Long) -> Unit, openDirectChat: (Long) -> Unit,
forwardItem: (ChatInfo, ChatItem) -> Unit,
updateContactStats: (Contact) -> Unit, updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit, updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> Unit, syncContactConnection: (Contact) -> Unit,
@@ -68,7 +69,8 @@ fun ChatItemView(
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
showItemDetails: (ChatInfo, ChatItem) -> Unit, showItemDetails: (ChatInfo, ChatItem) -> Unit,
developerTools: Boolean, developerTools: Boolean,
showViaProxy: Boolean showViaProxy: Boolean,
preview: Boolean = false,
) { ) {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val sent = cItem.chatDir.sent val sent = cItem.chatDir.sent
@@ -260,8 +262,7 @@ fun ChatItemView(
!cItem.isLiveDummy && !live !cItem.isLiveDummy && !live
) { ) {
ItemAction(stringResource(MR.strings.forward_chat_item), painterResource(MR.images.ic_forward), onClick = { ItemAction(stringResource(MR.strings.forward_chat_item), painterResource(MR.images.ic_forward), onClick = {
chatModel.chatId.value = null forwardItem(cInfo, cItem)
chatModel.sharedContent.value = SharedContent.Forward(cItem, cInfo)
showMenu.value = false showMenu.value = false
}) })
} }
@@ -272,7 +273,7 @@ fun ChatItemView(
if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null && !cItem.localNote) { if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null && !cItem.localNote) {
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction) CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction)
} }
if (!(live && cItem.meta.isLive)) { if (!(live && cItem.meta.isLive) && !preview) {
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
} }
val groupInfo = cItem.memberToModerate(cInfo)?.first val groupInfo = cItem.memberToModerate(cInfo)?.first
@@ -819,40 +820,40 @@ expect fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager)
@Preview @Preview
@Composable @Composable
fun PreviewChatItemView() { fun PreviewChatItemView(
SimpleXTheme { chatItem: ChatItem = ChatItem.getSampleData(1, CIDirection.DirectSnd(), Clock.System.now(), "hello")
ChatItemView( ) {
rhId = null, ChatItemView(
ChatInfo.Direct.sampleData, rhId = null,
ChatItem.getSampleData( ChatInfo.Direct.sampleData,
1, CIDirection.DirectSnd(), Clock.System.now(), "hello" chatItem,
), useLinkPreviews = true,
useLinkPreviews = true, linkMode = SimplexLinkMode.DESCRIPTION,
linkMode = SimplexLinkMode.DESCRIPTION, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, revealed = remember { mutableStateOf(false) },
revealed = remember { mutableStateOf(false) }, range = 0..1,
range = 0..1, deleteMessage = { _, _ -> },
deleteMessage = { _, _ -> }, deleteMessages = { _ -> },
deleteMessages = { _ -> }, receiveFile = { _ -> },
receiveFile = { _ -> }, cancelFile = {},
cancelFile = {}, joinGroup = { _, _ -> },
joinGroup = { _, _ -> }, acceptCall = { _ -> },
acceptCall = { _ -> }, scrollToItem = {},
scrollToItem = {}, acceptFeature = { _, _, _ -> },
acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> },
openDirectChat = { _ -> }, forwardItem = { _, _ -> },
updateContactStats = { }, updateContactStats = { },
updateMemberStats = { _, _ -> }, updateMemberStats = { _, _ -> },
syncContactConnection = { }, syncContactConnection = { },
syncMemberConnection = { _, _ -> }, syncMemberConnection = { _, _ -> },
findModelChat = { null }, findModelChat = { null },
findModelMember = { null }, findModelMember = { null },
setReaction = { _, _, _, _ -> }, setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> }, showItemDetails = { _, _ -> },
developerTools = false, developerTools = false,
showViaProxy = false showViaProxy = false,
) preview = true,
} )
} }
@Preview @Preview
@@ -877,6 +878,7 @@ fun PreviewChatItemViewDeletedContent() {
scrollToItem = {}, scrollToItem = {},
acceptFeature = { _, _, _ -> }, acceptFeature = { _, _, _ -> },
openDirectChat = { _ -> }, openDirectChat = { _ -> },
forwardItem = { _, _ -> },
updateContactStats = { }, updateContactStats = { },
updateMemberStats = { _, _ -> }, updateMemberStats = { _, _ -> },
syncContactConnection = { }, syncContactConnection = { },
@@ -886,7 +888,8 @@ fun PreviewChatItemViewDeletedContent() {
setReaction = { _, _, _, _ -> }, setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> }, showItemDetails = { _, _ -> },
developerTools = false, developerTools = false,
showViaProxy = false showViaProxy = false,
preview = true,
) )
} }
} }
@@ -18,8 +18,8 @@ import chat.simplex.common.ui.theme.*
@Composable @Composable
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) { fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) {
val sent = ci.chatDir.sent val sent = ci.chatDir.sent
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val sentColor = MaterialTheme.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage val receivedColor = MaterialTheme.appColors.receivedMessage
Surface( Surface(
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
color = if (sent) sentColor else receivedColor, color = if (sent) sentColor else receivedColor,
@@ -46,9 +46,6 @@ fun FramedItemView(
return if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.membership else null return if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.membership else null
} }
@Composable
fun Color.toQuote(): Color = if (isInDarkTheme()) lighter(0.12f) else darker(0.12f)
@Composable @Composable
fun ciQuotedMsgTextView(qi: CIQuote, lines: Int) { fun ciQuotedMsgTextView(qi: CIQuote, lines: Int) {
MarkdownText( MarkdownText(
@@ -89,11 +86,11 @@ fun FramedItemView(
@Composable @Composable
fun FramedItemHeader(caption: String, italic: Boolean, icon: Painter? = null, pad: Boolean = false) { fun FramedItemHeader(caption: String, italic: Boolean, icon: Painter? = null, pad: Boolean = false) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val sentColor = MaterialTheme.appColors.sentQuote
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage val receivedColor = MaterialTheme.appColors.receivedQuote
Row( Row(
Modifier Modifier
.background(if (sent) sentColor.toQuote() else receivedColor.toQuote()) .background(if (sent) sentColor else receivedColor)
.fillMaxWidth() .fillMaxWidth()
.padding(start = 8.dp, top = 6.dp, end = 12.dp, bottom = if (pad || (ci.quotedItem == null && ci.meta.itemForwarded == null)) 6.dp else 0.dp), .padding(start = 8.dp, top = 6.dp, end = 12.dp, bottom = if (pad || (ci.quotedItem == null && ci.meta.itemForwarded == null)) 6.dp else 0.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
@@ -122,11 +119,11 @@ fun FramedItemView(
@Composable @Composable
fun ciQuoteView(qi: CIQuote) { fun ciQuoteView(qi: CIQuote) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val sentColor = MaterialTheme.appColors.sentQuote
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage val receivedColor = MaterialTheme.appColors.receivedQuote
Row( Row(
Modifier Modifier
.background(if (sent) sentColor.toQuote() else receivedColor.toQuote()) .background(if (sent) sentColor else receivedColor)
.fillMaxWidth() .fillMaxWidth()
.combinedClickable( .combinedClickable(
onLongClick = { showMenu.value = true }, onLongClick = { showMenu.value = true },
@@ -188,10 +185,17 @@ fun FramedItemView(
val transparentBackground = (ci.content.msgContent is MsgContent.MCImage || ci.content.msgContent is MsgContent.MCVideo) && val transparentBackground = (ci.content.msgContent is MsgContent.MCImage || ci.content.msgContent is MsgContent.MCVideo) &&
!ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null && ci.meta.itemForwarded == null !ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null && ci.meta.itemForwarded == null
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val sentColor = MaterialTheme.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage val receivedColor = MaterialTheme.appColors.receivedMessage
Box(Modifier Box(Modifier
.clip(RoundedCornerShape(18.dp)) .clip(RoundedCornerShape(18.dp))
.background(
when {
transparentBackground -> Color.Transparent
sent -> MaterialTheme.colors.background
else -> MaterialTheme.colors.background
}
)
.background( .background(
when { when {
transparentBackground -> Color.Transparent transparentBackground -> Color.Transparent
@@ -17,8 +17,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.common.model.ChatItem import chat.simplex.common.model.ChatItem
import chat.simplex.common.model.MsgErrorType import chat.simplex.common.model.MsgErrorType
import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.ui.theme.*
import chat.simplex.common.ui.theme.SimpleXTheme
import chat.simplex.common.views.helpers.AlertManager import chat.simplex.common.views.helpers.AlertManager
import chat.simplex.common.views.helpers.generalGetString import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR import chat.simplex.res.MR
@@ -51,7 +50,7 @@ fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTT
@Composable @Composable
fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) { fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) {
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage val receivedColor = MaterialTheme.appColors.receivedMessage
Surface( Surface(
Modifier.clickable(onClick = onClick), Modifier.clickable(onClick = onClick),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
@@ -21,8 +21,8 @@ import kotlinx.datetime.Clock
@Composable @Composable
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState<Boolean>, showViaProxy: Boolean) { fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState<Boolean>, showViaProxy: Boolean) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val sentColor = MaterialTheme.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage val receivedColor = MaterialTheme.appColors.receivedMessage
Surface( Surface(
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
color = if (ci.chatDir.sent) sentColor else receivedColor, color = if (ci.chatDir.sent) sentColor else receivedColor,
@@ -78,7 +78,7 @@ fun MarkdownText (
val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) { val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) {
"\n" "\n"
} else if (meta != null) { } else if (meta != null) {
reserveSpaceForMeta(meta, chatTTL, null, showViaProxy = showViaProxy) reserveSpaceForMeta(meta, chatTTL, null, secondaryColor = MaterialTheme.colors.secondary, showViaProxy = showViaProxy)
} else { } else {
" " " "
} }
@@ -486,6 +486,8 @@ fun deleteChatDatabaseFilesAndState() {
tmpDir.deleteRecursively() tmpDir.deleteRecursively()
getMigrationTempFilesDirectory().deleteRecursively() getMigrationTempFilesDirectory().deleteRecursively()
tmpDir.mkdir() tmpDir.mkdir()
wallpapersDir.deleteRecursively()
wallpapersDir.mkdirs()
DatabaseUtils.ksDatabasePassword.remove() DatabaseUtils.ksDatabasePassword.remove()
controller.appPrefs.storeDBPassphrase.set(true) controller.appPrefs.storeDBPassphrase.set(true)
controller.ctrl = null controller.ctrl = null
@@ -2,12 +2,14 @@ package chat.simplex.common.views.helpers
import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.* import androidx.compose.foundation.shape.*
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
@@ -51,7 +53,8 @@ fun ProfileImage(
size: Dp, size: Dp,
image: String? = null, image: String? = null,
icon: ImageResource = MR.images.ic_account_circle_filled, icon: ImageResource = MR.images.ic_account_circle_filled,
color: Color = MaterialTheme.colors.secondaryVariant color: Color = MaterialTheme.colors.secondaryVariant,
backgroundColor: Color? = null
) { ) {
Box(Modifier.size(size)) { Box(Modifier.size(size)) {
if (image == null) { if (image == null) {
@@ -61,6 +64,9 @@ fun ProfileImage(
else -> null else -> null
} }
if (iconToReplace != null) { if (iconToReplace != null) {
if (backgroundColor != null) {
Box(Modifier.size(size * 0.7f).align(Alignment.Center).background(backgroundColor, CircleShape))
}
Icon( Icon(
iconToReplace, iconToReplace,
contentDescription = stringResource(MR.strings.icon_descr_profile_image_placeholder), contentDescription = stringResource(MR.strings.icon_descr_profile_image_placeholder),
@@ -0,0 +1,418 @@
package chat.simplex.common.views.helpers
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.io.File
import kotlin.math.*
enum class PresetWallpaper(
val res: ImageResource,
val filename: String,
val text: StringResource,
val scale: Float,
val background: Map<DefaultTheme, Color>,
val tint: Map<DefaultTheme, Color>,
val colors: Map<DefaultTheme, ThemeColors>,
) {
CATS(MR.images.wallpaper_cats, "cats", MR.strings.wallpaper_cats, 0.63f,
wallpaperBackgrounds(light = "#ffF8F6EA"),
tint = mapOf(
DefaultTheme.LIGHT to "#ffefdca6".colorFromReadableHex(),
DefaultTheme.DARK to "#ff4b3b0e".colorFromReadableHex(),
DefaultTheme.SIMPLEX to "#ff51400f".colorFromReadableHex(),
DefaultTheme.BLACK to "#ff4b3b0e".colorFromReadableHex()
),
mapOf(
DefaultTheme.LIGHT to ThemeColors(
sentMessage = "#fffffaed",
sentQuote = "#fffaf0d6",
receivedMessage = "#ffF8F7F4",
receivedQuote = "#ffefede9",
),
DefaultTheme.DARK to ThemeColors(
sentMessage = "#ff2f2919",
sentQuote = "#ff473a1d",
receivedMessage = "#ff272624",
receivedQuote = "#ff373633",
),
DefaultTheme.SIMPLEX to ThemeColors(
sentMessage = "#ff41371b",
sentQuote = "#ff654f1c",
receivedMessage = "#ff272624",
receivedQuote = "#ff373633",
),
DefaultTheme.BLACK to ThemeColors(
sentMessage = "#ff41371b",
sentQuote = "#ff654f1c",
receivedMessage = "#ff1f1e1b",
receivedQuote = "#ff2f2d27",
),
)
),
FLOWERS(MR.images.wallpaper_flowers, "flowers", MR.strings.wallpaper_flowers, 0.53f,
wallpaperBackgrounds(light = "#ffE2FFE4"),
tint = mapOf(
DefaultTheme.LIGHT to "#ff9CEA59".colorFromReadableHex(),
DefaultTheme.DARK to "#ff31560D".colorFromReadableHex(),
DefaultTheme.SIMPLEX to "#ff36600f".colorFromReadableHex(),
DefaultTheme.BLACK to "#ff31560D".colorFromReadableHex()
),
mapOf(
DefaultTheme.LIGHT to ThemeColors(
sentMessage = "#fff1ffe5",
sentQuote = "#ffdcf9c4",
receivedMessage = "#ffF4F8F2",
receivedQuote = "#ffe7ece7",
),
DefaultTheme.DARK to ThemeColors(
sentMessage = "#ff163521",
sentQuote = "#ff1B5330",
receivedMessage = "#ff242523",
receivedQuote = "#ff353733",
),
DefaultTheme.SIMPLEX to ThemeColors(
sentMessage = "#ff184739",
sentQuote = "#ff1F6F4B",
receivedMessage = "#ff242523",
receivedQuote = "#ff353733",
),
DefaultTheme.BLACK to ThemeColors(
sentMessage = "#ff184739",
sentQuote = "#ff1F6F4B",
receivedMessage = "#ff1c1f1a",
receivedQuote = "#ff282b25",
),
)
),
HEARTS(MR.images.wallpaper_hearts, "hearts", MR.strings.wallpaper_hearts, 0.59f,
wallpaperBackgrounds(light = "#ffFDECEC"),
tint = mapOf(
DefaultTheme.LIGHT to "#fffde0e0".colorFromReadableHex(),
DefaultTheme.DARK to "#ff3c0f0f".colorFromReadableHex(),
DefaultTheme.SIMPLEX to "#ff411010".colorFromReadableHex(),
DefaultTheme.BLACK to "#ff3C0F0F".colorFromReadableHex()
),
mapOf(
DefaultTheme.LIGHT to ThemeColors(
sentMessage = "#fffff4f4",
sentQuote = "#ffffdfdf",
receivedMessage = "#fff8f6f6",
receivedQuote = "#ffefebeb",
),
DefaultTheme.DARK to ThemeColors(
sentMessage = "#ff301515",
sentQuote = "#ff4C1818",
receivedMessage = "#ff242121",
receivedQuote = "#ff3b3535",
),
DefaultTheme.SIMPLEX to ThemeColors(
sentMessage = "#ff491A28",
sentQuote = "#ff761F29",
receivedMessage = "#ff242121",
receivedQuote = "#ff3b3535",
),
DefaultTheme.BLACK to ThemeColors(
sentMessage = "#ff491A28",
sentQuote = "#ff761F29",
receivedMessage = "#ff1f1b1b",
receivedQuote = "#ff2e2626",
),
)
),
KIDS(MR.images.wallpaper_kids, "kids", MR.strings.wallpaper_kids, 0.53f,
wallpaperBackgrounds(light = "#ffdbfdfb"),
tint = mapOf(
DefaultTheme.LIGHT to "#ffadeffc".colorFromReadableHex(),
DefaultTheme.DARK to "#ff16404B".colorFromReadableHex(),
DefaultTheme.SIMPLEX to "#ff184753".colorFromReadableHex(),
DefaultTheme.BLACK to "#ff16404B".colorFromReadableHex()
),
mapOf(
DefaultTheme.LIGHT to ThemeColors(
sentMessage = "#ffeafeff",
sentQuote = "#ffcbf4f7",
receivedMessage = "#fff3fafa",
receivedQuote = "#ffe4efef",
),
DefaultTheme.DARK to ThemeColors(
sentMessage = "#ff16302F",
sentQuote = "#ff1a4a49",
receivedMessage = "#ff252626",
receivedQuote = "#ff373A39",
),
DefaultTheme.SIMPLEX to ThemeColors(
sentMessage = "#ff1a4745",
sentQuote = "#ff1d6b69",
receivedMessage = "#ff252626",
receivedQuote = "#ff373a39",
),
DefaultTheme.BLACK to ThemeColors(
sentMessage = "#ff1a4745",
sentQuote = "#ff1d6b69",
receivedMessage = "#ff1e1f1f",
receivedQuote = "#ff262b29",
),
)
),
SCHOOL(MR.images.wallpaper_school, "school", MR.strings.wallpaper_school, 0.53f,
wallpaperBackgrounds(light = "#ffE7F5FF"),
tint = mapOf(
DefaultTheme.LIGHT to "#ffCEEBFF".colorFromReadableHex(),
DefaultTheme.DARK to "#ff0F293B".colorFromReadableHex(),
DefaultTheme.SIMPLEX to "#ff112f43".colorFromReadableHex(),
DefaultTheme.BLACK to "#ff0F293B".colorFromReadableHex()
),
mapOf(
DefaultTheme.LIGHT to ThemeColors(
sentMessage = "#ffeef9ff",
sentQuote = "#ffD6EDFA",
receivedMessage = "#ffF3F5F9",
receivedQuote = "#ffe4e8ee",
),
DefaultTheme.DARK to ThemeColors(
sentMessage = "#ff172833",
sentQuote = "#ff1C3E4F",
receivedMessage = "#ff26282c",
receivedQuote = "#ff393c40",
),
DefaultTheme.SIMPLEX to ThemeColors(
sentMessage = "#ff1A3C5D",
sentQuote = "#ff235b80",
receivedMessage = "#ff26282c",
receivedQuote = "#ff393c40",
),
DefaultTheme.BLACK to ThemeColors(
sentMessage = "#ff1A3C5D",
sentQuote = "#ff235b80",
receivedMessage = "#ff1d1e22",
receivedQuote = "#ff292b2f",
),
)
),
TRAVEL(MR.images.wallpaper_travel, "travel", MR.strings.wallpaper_travel, 0.68f,
wallpaperBackgrounds(light = "#fff9eeff"),
tint = mapOf(
DefaultTheme.LIGHT to "#ffeedbfe".colorFromReadableHex(),
DefaultTheme.DARK to "#ff311E48".colorFromReadableHex(),
DefaultTheme.SIMPLEX to "#ff35204e".colorFromReadableHex(),
DefaultTheme.BLACK to "#ff311E48".colorFromReadableHex()
),
mapOf(
DefaultTheme.LIGHT to ThemeColors(
sentMessage = "#fffcf6ff",
sentQuote = "#fff2e0fc",
receivedMessage = "#ffF6F4F7",
receivedQuote = "#ffede9ee",
),
DefaultTheme.DARK to ThemeColors(
sentMessage = "#ff33263B",
sentQuote = "#ff53385E",
receivedMessage = "#ff272528",
receivedQuote = "#ff3B373E",
),
DefaultTheme.SIMPLEX to ThemeColors(
sentMessage = "#ff3C255D",
sentQuote = "#ff623485",
receivedMessage = "#ff26273B",
receivedQuote = "#ff3A394F",
),
DefaultTheme.BLACK to ThemeColors(
sentMessage = "#ff3C255D",
sentQuote = "#ff623485",
receivedMessage = "#ff231f23",
receivedQuote = "#ff2c2931",
),
)
);
fun toType(base: DefaultTheme, scale: Float? = null): WallpaperType =
WallpaperType.Preset(
filename,
scale ?: appPrefs.themeOverrides.get().firstOrNull { it.wallpaper != null && it.wallpaper.preset == filename && it.base == base }?.wallpaper?.scale ?: 1f
)
companion object {
fun from(filename: String): PresetWallpaper? =
entries.firstOrNull { it.filename == filename }
}
}
fun wallpaperBackgrounds(light: String): Map<DefaultTheme, Color> =
mapOf(
DefaultTheme.LIGHT to light.colorFromReadableHex(),
DefaultTheme.DARK to "#ff121212".colorFromReadableHex(),
DefaultTheme.SIMPLEX to "#ff111528".colorFromReadableHex(),
DefaultTheme.BLACK to "#ff070707".colorFromReadableHex()
)
@Serializable
enum class WallpaperScaleType(val contentScale: ContentScale, val text: StringResource) {
@SerialName("fill") FILL(ContentScale.Crop, MR.strings.wallpaper_scale_fill),
@SerialName("fit") FIT(ContentScale.Fit, MR.strings.wallpaper_scale_fit),
@SerialName("repeat") REPEAT(ContentScale.Fit, MR.strings.wallpaper_scale_repeat),
}
sealed class WallpaperType {
abstract val scale: Float?
val image by lazy {
val filename = when (this) {
is Preset -> filename
is Image -> filename
else -> return@lazy null
}
if (filename == "") return@lazy null
if (cachedImages[filename] != null) {
cachedImages[filename]
} else {
val res = if (this is Preset) {
(PresetWallpaper.from(filename) ?: PresetWallpaper.CATS).res.toComposeImageBitmap()!!
} else {
try {
// In case of unintentional image deletion don't crash the app
File(getWallpaperFilePath(filename)).inputStream().use { loadImageBitmap(it) }
} catch (e: Exception) {
Log.e(TAG, "Error while loading wallpaper file: ${e.stackTraceToString()}")
null
}
}
res?.prepareToDraw()
cachedImages[filename] = res ?: return@lazy null
res
}
}
fun sameType(other: WallpaperType?): Boolean =
if (this is Preset && other is Preset) this.filename == other.filename
else this.javaClass == other?.javaClass
fun samePreset(other: PresetWallpaper?): Boolean = this is Preset && filename == other?.filename
data class Preset(
val filename: String,
override val scale: Float?,
): WallpaperType() {
val predefinedImageScale = PresetWallpaper.from(filename)?.scale ?: 1f
}
data class Image(
val filename: String,
override val scale: Float?,
val scaleType: WallpaperScaleType?,
): WallpaperType()
object Empty: WallpaperType() {
override val scale: Float?
get() = null
}
fun defaultBackgroundColor(theme: DefaultTheme, materialBackground: Color): Color =
if (this is Preset) {
(PresetWallpaper.from(filename) ?: PresetWallpaper.CATS).background[theme]!!
} else {
materialBackground
}
fun defaultTintColor(theme: DefaultTheme): Color =
if (this is Preset) {
(PresetWallpaper.from(filename) ?: PresetWallpaper.CATS).tint[theme]!!
} else if (this is Image && scaleType == WallpaperScaleType.REPEAT) {
Color.Transparent
} else {
Color.Transparent
}
companion object {
var cachedImages: MutableMap<String, ImageBitmap> = mutableMapOf()
fun from(wallpaper: ThemeWallpaper?): WallpaperType? {
return if (wallpaper == null) {
null
} else if (wallpaper.preset != null) {
Preset(wallpaper.preset, wallpaper.scale)
} else if (wallpaper.imageFile != null) {
Image(wallpaper.imageFile, wallpaper.scale, wallpaper.scaleType)
} else {
Empty
}
}
}
}
fun DrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperType, background: Color, tint: Color) = clipRect {
val quality = FilterQuality.High
fun repeat(imageScale: Float) {
val scale = imageScale * density
for (h in 0..(size.height / image.height / scale).roundToInt()) {
for (w in 0..(size.width / image.width / scale).roundToInt()) {
drawImage(
image,
dstOffset = IntOffset(x = (w * image.width * scale).roundToInt(), y = (h * image.height * scale).roundToInt()),
dstSize = IntSize((image.width * scale).roundToInt(), (image.height * scale).roundToInt()),
colorFilter = ColorFilter.tint(tint, BlendMode.SrcIn),
filterQuality = quality
)
}
}
}
drawRect(background)
when (imageType) {
is WallpaperType.Preset -> repeat((imageType.scale ?: 1f) * imageType.predefinedImageScale)
is WallpaperType.Image -> when (val scaleType = imageType.scaleType ?: WallpaperScaleType.FILL) {
WallpaperScaleType.REPEAT -> repeat(imageType.scale ?: 1f)
WallpaperScaleType.FILL, WallpaperScaleType.FIT -> {
val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height))
val scaledWidth = (image.width * scale.scaleX).roundToInt()
val scaledHeight = (image.height * scale.scaleY).roundToInt()
// Large image will cause freeze
if (image.width > 4320 || image.height > 4320) return@clipRect
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
}
}
}
drawRect(tint)
}
}
is WallpaperType.Empty -> {}
}
}
@@ -51,7 +51,7 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co
@Composable @Composable
fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f) { fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f) {
val theme = CurrentColors.collectAsState() val theme = CurrentColors.collectAsState()
val titleColor = CurrentColors.collectAsState().value.appColors.title val titleColor = MaterialTheme.appColors.title
val brush = if (theme.value.base == DefaultTheme.SIMPLEX) val brush = if (theme.value.base == DefaultTheme.SIMPLEX)
Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
else // color is not updated when changing themes if I pass null here else // color is not updated when changing themes if I pass null here
@@ -76,7 +76,7 @@ suspend fun getLinkPreview(url: String): LinkPreview? {
@Composable @Composable
fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancelEnabled: Boolean) { fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancelEnabled: Boolean) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val sentColor = MaterialTheme.appColors.sentMessage
Row( Row(
Modifier.fillMaxWidth().padding(top = 8.dp).background(sentColor), Modifier.fillMaxWidth().padding(top = 8.dp).background(sentColor),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@@ -41,9 +41,12 @@ enum class ModalPlacement {
} }
class ModalData { class ModalData {
private val state = mutableMapOf<String, MutableState<Any>>() private val state = mutableMapOf<String, MutableState<Any?>>()
fun <T> stateGetOrPut (key: String, default: () -> T): MutableState<T> = fun <T> stateGetOrPut (key: String, default: () -> T): MutableState<T> =
state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState<T> state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState<T>
fun <T> stateGetOrPutNullable (key: String, default: () -> T?): MutableState<T?> =
state.getOrPut(key) { mutableStateOf(default() as Any?) } as MutableState<T?>
} }
class ModalManager(private val placement: ModalPlacement? = null) { class ModalManager(private val placement: ModalPlacement? = null) {
@@ -0,0 +1,520 @@
package chat.simplex.common.views.helpers
import SectionBottomSpacer
import SectionItemView
import SectionSpacer
import SectionView
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.MaterialTheme.colors
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.yaml
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.usersettings.*
import chat.simplex.common.views.usersettings.AppearanceScope.WallpaperPresetSelector
import chat.simplex.common.views.usersettings.AppearanceScope.editColor
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.serialization.encodeToString
import java.net.URI
@Composable
fun ModalData.UserWallpaperEditor(
theme: ThemeModeOverride,
applyToMode: DefaultThemeMode?,
globalThemeUsed: MutableState<Boolean>,
save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit
) {
ColumnWithScrollBar(
Modifier
.fillMaxSize()
) {
val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } }
var showMore by remember { stateGetOrPut("showMore") { false } }
val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } }
val currentTheme by CurrentColors.collectAsState()
AppBarTitle(stringResource(MR.strings.settings_section_title_user_theme))
val wallpaperImage = MaterialTheme.wallpaper.type.image
val wallpaperType = MaterialTheme.wallpaper.type
val onTypeCopyFromSameTheme = { type: WallpaperType? ->
if (type is WallpaperType.Image && chatModel.remoteHostId() != null) {
false
} else {
ThemeManager.copyFromSameThemeOverrides(type, null, themeModeOverride)
withBGApi { save(applyToMode.value, themeModeOverride.value) }
globalThemeUsed.value = false
true
}
}
val preApplyGlobalIfNeeded = { type: WallpaperType? ->
if (globalThemeUsed.value) {
onTypeCopyFromSameTheme(type)
}
}
val onTypeChange: (WallpaperType?) -> Unit = { type: WallpaperType? ->
if (globalThemeUsed.value) {
preApplyGlobalIfNeeded(type)
// Saves copied static image instead of original from global theme
ThemeManager.applyWallpaper(themeModeOverride.value.type, themeModeOverride)
} else {
ThemeManager.applyWallpaper(type, themeModeOverride)
}
withBGApi { save(applyToMode.value, themeModeOverride.value) }
}
val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? ->
if (to != null) {
val filename = saveWallpaperFile(to)
if (filename != null) {
onTypeChange(WallpaperType.Image(filename, 1f, WallpaperScaleType.FILL))
}
}
}
val currentColors = { type: WallpaperType? ->
// If applying for :
// - all themes: no overrides needed
// - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected
val perUserOverride = if (wallpaperType.sameType(type)) chatModel.currentUser.value?.uiThemes else null
ThemeManager.currentColors(type, null, perUserOverride, appPrefs.themeOverrides.get())
}
val onChooseType: (WallpaperType?) -> Unit = { type: WallpaperType? ->
when {
// don't have image in parent or already selected wallpaper with custom image
type is WallpaperType.Image && chatModel.remoteHostId() != null -> { /* do nothing */ }
type is WallpaperType.Image && (wallpaperType is WallpaperType.Image || currentColors(type).wallpaper.type.image == null) -> withLongRunningApi { importWallpaperLauncher.launch("image/*") }
type is WallpaperType.Image -> onTypeCopyFromSameTheme(currentColors(type).wallpaper.type)
themeModeOverride.value.type != type || currentTheme.wallpaper.type != type -> onTypeCopyFromSameTheme(type)
else -> onTypeChange(type)
}
}
val editColor = { name: ThemeColor ->
editColor(
name,
wallpaperType,
wallpaperImage,
onColorChange = { color ->
preApplyGlobalIfNeeded(themeModeOverride.value.type)
ThemeManager.applyThemeColor(name, color, themeModeOverride)
withBGApi { save(applyToMode.value, themeModeOverride.value) }
}
)
}
WallpaperPresetSelector(
selectedWallpaper = wallpaperType,
baseTheme = currentTheme.base,
currentColors = { type ->
// If applying for :
// - all themes: no overrides needed
// - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected
val perUserOverride = if (wallpaperType.sameType(type)) chatModel.currentUser.value?.uiThemes else null
ThemeManager.currentColors(type, null, perUserOverride, appPrefs.themeOverrides.get())
},
onChooseType = onChooseType
)
WallpaperSetupView(
themeModeOverride.value.type,
CurrentColors.collectAsState().value.base,
currentTheme.wallpaper,
currentTheme.appColors.sentMessage,
currentTheme.appColors.sentQuote,
currentTheme.appColors.receivedMessage,
currentTheme.appColors.receivedQuote,
editColor = { name -> editColor(name) },
onTypeChange = onTypeChange,
)
SectionSpacer()
if (!globalThemeUsed.value) {
ResetToGlobalThemeButton(true) {
themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
globalThemeUsed.value = true
withBGApi { save(applyToMode.value, null) }
}
}
SetDefaultThemeButton {
globalThemeUsed.value = false
val lightBase = DefaultTheme.LIGHT
val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX
val mode = themeModeOverride.value.mode
withBGApi {
// Saving for both modes in one place by changing mode once per save
if (applyToMode.value == null) {
val oppositeMode = if (mode == DefaultThemeMode.LIGHT) DefaultThemeMode.DARK else DefaultThemeMode.LIGHT
save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, if (oppositeMode == DefaultThemeMode.LIGHT) lightBase else darkBase))
}
themeModeOverride.value = ThemeModeOverride.withFilledAppDefaults(mode, if (mode == DefaultThemeMode.LIGHT) lightBase else darkBase)
save(themeModeOverride.value.mode, themeModeOverride.value)
}
}
KeyChangeEffect(theme.mode) {
themeModeOverride.value = theme
if (applyToMode.value != null) {
applyToMode.value = theme.mode
}
}
// Applies updated global theme if current one tracks global theme
KeyChangeEffect(CurrentColors.collectAsState().value) {
if (globalThemeUsed.value) {
themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
globalThemeUsed.value = true
}
}
SectionSpacer()
if (showMore) {
val values by remember { mutableStateOf(
listOf(
null to generalGetString(MR.strings.chat_theme_apply_to_all_modes),
DefaultThemeMode.LIGHT to generalGetString(MR.strings.chat_theme_apply_to_light_mode),
DefaultThemeMode.DARK to generalGetString(MR.strings.chat_theme_apply_to_dark_mode),
)
)
}
ExposedDropDownSettingRow(
generalGetString(MR.strings.chat_theme_apply_to_mode),
values,
applyToMode,
icon = null,
enabled = remember { mutableStateOf(true) },
onSelected = {
applyToMode.value = it
if (it != null && it != CurrentColors.value.base.mode) {
val lightBase = DefaultTheme.LIGHT
val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX
ThemeManager.applyTheme(if (it == DefaultThemeMode.LIGHT) lightBase.themeName else darkBase.themeName)
}
}
)
SectionSpacer()
AppearanceScope.CustomizeThemeColorsSection(currentTheme, editColor = editColor)
SectionSpacer()
ImportExportThemeSection(null, remember { chatModel.currentUser }.value?.uiThemes) {
withBGApi {
themeModeOverride.value = it
save(applyToMode.value, it)
}
}
} else {
AdvancedSettingsButton { showMore = true }
}
SectionBottomSpacer()
}
}
@Composable
fun ModalData.ChatWallpaperEditor(
theme: ThemeModeOverride,
applyToMode: DefaultThemeMode?,
globalThemeUsed: MutableState<Boolean>,
save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit
) {
ColumnWithScrollBar(
Modifier
.fillMaxSize()
) {
val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } }
var showMore by remember { stateGetOrPut("showMore") { false } }
val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } }
val currentTheme by remember(themeModeOverride.value, CurrentColors.collectAsState().value) {
mutableStateOf(
ThemeManager.currentColors(null, if (globalThemeUsed.value) null else themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())
)
}
AppBarTitle(stringResource(MR.strings.settings_section_title_chat_theme))
val onTypeCopyFromSameTheme: (WallpaperType?) -> Boolean = { type ->
if (type is WallpaperType.Image && chatModel.remoteHostId() != null) {
false
} else {
val success = ThemeManager.copyFromSameThemeOverrides(type, chatModel.currentUser.value?.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight), themeModeOverride)
if (success) {
withBGApi { save(applyToMode.value, themeModeOverride.value) }
globalThemeUsed.value = false
}
success
}
}
val preApplyGlobalIfNeeded = { type: WallpaperType? ->
if (globalThemeUsed.value) {
onTypeCopyFromSameTheme(type)
}
}
val onTypeChange: (WallpaperType?) -> Unit = { type ->
if (globalThemeUsed.value) {
preApplyGlobalIfNeeded(type)
// Saves copied static image instead of original from global theme
ThemeManager.applyWallpaper(themeModeOverride.value.type, themeModeOverride)
} else {
ThemeManager.applyWallpaper(type, themeModeOverride)
}
withBGApi { save(applyToMode.value, themeModeOverride.value) }
}
val editColor: (ThemeColor) -> Unit = { name: ThemeColor ->
ModalManager.end.showModal {
val currentTheme by remember(themeModeOverride.value, CurrentColors.collectAsState().value) {
mutableStateOf(
ThemeManager.currentColors(null, themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())
)
}
val initialColor: Color = when (name) {
ThemeColor.WALLPAPER_BACKGROUND -> currentTheme.wallpaper.background ?: Color.Transparent
ThemeColor.WALLPAPER_TINT -> currentTheme.wallpaper.tint ?: Color.Transparent
ThemeColor.PRIMARY -> currentTheme.colors.primary
ThemeColor.PRIMARY_VARIANT -> currentTheme.colors.primaryVariant
ThemeColor.SECONDARY -> currentTheme.colors.secondary
ThemeColor.SECONDARY_VARIANT -> currentTheme.colors.secondaryVariant
ThemeColor.BACKGROUND -> currentTheme.colors.background
ThemeColor.SURFACE -> currentTheme.colors.surface
ThemeColor.TITLE -> currentTheme.appColors.title
ThemeColor.PRIMARY_VARIANT2 -> currentTheme.appColors.primaryVariant2
ThemeColor.SENT_MESSAGE -> currentTheme.appColors.sentMessage
ThemeColor.SENT_QUOTE -> currentTheme.appColors.sentQuote
ThemeColor.RECEIVED_MESSAGE -> currentTheme.appColors.receivedMessage
ThemeColor.RECEIVED_QUOTE -> currentTheme.appColors.receivedQuote
}
AppearanceScope.ColorEditor(
name,
initialColor,
CurrentColors.collectAsState().value.base,
themeModeOverride.value.type,
themeModeOverride.value.type?.image,
currentTheme.wallpaper.background,
currentTheme.wallpaper.tint,
currentColors = {
ThemeManager.currentColors(null, themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())
},
onColorChange = { color ->
preApplyGlobalIfNeeded(themeModeOverride.value.type)
ThemeManager.applyThemeColor(name, color, themeModeOverride)
withBGApi { save(applyToMode.value, themeModeOverride.value) }
}
)
}
}
val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? ->
if (to != null) {
val filename = saveWallpaperFile(to)
if (filename != null) {
// Delete only non-user image
if (!globalThemeUsed.value) {
removeWallpaperFile((themeModeOverride.value.type as? WallpaperType.Image)?.filename)
}
globalThemeUsed.value = false
onTypeChange(WallpaperType.Image(filename, 1f, WallpaperScaleType.FILL))
}
}
}
val currentColors = { type: WallpaperType? ->
ThemeManager.currentColors(type, if (type?.sameType(themeModeOverride.value.type) == true) themeModeOverride.value else null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
}
WallpaperPresetSelector(
selectedWallpaper = currentTheme.wallpaper.type,
activeBackgroundColor = currentTheme.wallpaper.background,
activeTintColor = currentTheme.wallpaper.tint,
baseTheme = CurrentColors.collectAsState().value.base,
currentColors = { type -> currentColors(type) },
onChooseType = { type ->
when {
type is WallpaperType.Image && chatModel.remoteHostId() != null -> { /* do nothing */ }
type is WallpaperType.Image && ((themeModeOverride.value.type is WallpaperType.Image && !globalThemeUsed.value) || currentColors(type).wallpaper.type.image == null) -> {
withLongRunningApi { importWallpaperLauncher.launch("image/*") }
}
type is WallpaperType.Image -> {
if (!onTypeCopyFromSameTheme(currentColors(type).wallpaper.type)) {
withLongRunningApi { importWallpaperLauncher.launch("image/*") }
}
}
globalThemeUsed.value || themeModeOverride.value.type != type -> {
onTypeCopyFromSameTheme(type)
}
else -> {
onTypeChange(type)
}
}
},
)
WallpaperSetupView(
themeModeOverride.value.type,
CurrentColors.collectAsState().value.base,
currentTheme.wallpaper,
currentTheme.appColors.sentMessage,
currentTheme.appColors.sentQuote,
currentTheme.appColors.receivedMessage,
currentTheme.appColors.receivedQuote,
editColor = editColor,
onTypeChange = onTypeChange,
)
SectionSpacer()
if (!globalThemeUsed.value) {
ResetToGlobalThemeButton(remember { chatModel.currentUser }.value?.uiThemes?.preferredMode(isInDarkTheme()) == null) {
themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
globalThemeUsed.value = true
withBGApi { save(applyToMode.value, null) }
}
}
SetDefaultThemeButton {
globalThemeUsed.value = false
val lightBase = DefaultTheme.LIGHT
val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX
val mode = themeModeOverride.value.mode
withBGApi {
// Saving for both modes in one place by changing mode once per save
if (applyToMode.value == null) {
val oppositeMode = if (mode == DefaultThemeMode.LIGHT) DefaultThemeMode.DARK else DefaultThemeMode.LIGHT
save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, if (oppositeMode == DefaultThemeMode.LIGHT) lightBase else darkBase))
}
themeModeOverride.value = ThemeModeOverride.withFilledAppDefaults(mode, if (mode == DefaultThemeMode.LIGHT) lightBase else darkBase)
save(themeModeOverride.value.mode, themeModeOverride.value)
}
}
KeyChangeEffect(theme.mode) {
themeModeOverride.value = theme
if (applyToMode.value != null) {
applyToMode.value = theme.mode
}
}
// Applies updated global theme if current one tracks global theme
KeyChangeEffect(CurrentColors.collectAsState()) {
if (globalThemeUsed.value) {
themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
globalThemeUsed.value = true
}
}
SectionSpacer()
if (showMore) {
val values by remember { mutableStateOf(
listOf(
null to generalGetString(MR.strings.chat_theme_apply_to_all_modes),
DefaultThemeMode.LIGHT to generalGetString(MR.strings.chat_theme_apply_to_light_mode),
DefaultThemeMode.DARK to generalGetString(MR.strings.chat_theme_apply_to_dark_mode),
)
)
}
ExposedDropDownSettingRow(
generalGetString(MR.strings.chat_theme_apply_to_mode),
values,
applyToMode,
icon = null,
enabled = remember { mutableStateOf(true) },
onSelected = {
applyToMode.value = it
if (it != null && it != CurrentColors.value.base.mode) {
val lightBase = DefaultTheme.LIGHT
val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX
ThemeManager.applyTheme(if (it == DefaultThemeMode.LIGHT) lightBase.themeName else darkBase.themeName)
}
}
)
SectionSpacer()
AppearanceScope.CustomizeThemeColorsSection(currentTheme, editColor = editColor)
SectionSpacer()
ImportExportThemeSection(themeModeOverride.value, remember { chatModel.currentUser }.value?.uiThemes) {
withBGApi {
themeModeOverride.value = it
save(applyToMode.value, it)
}
}
} else {
AdvancedSettingsButton { showMore = true }
}
SectionBottomSpacer()
}
}
@Composable
private fun ImportExportThemeSection(perChat: ThemeModeOverride?, perUser: ThemeModeOverrides?, save: (ThemeModeOverride) -> Unit) {
SectionView {
val theme = remember { mutableStateOf(null as String?) }
val exportThemeLauncher = rememberFileChooserLauncher(false) { to: URI? ->
val themeValue = theme.value
if (themeValue != null && to != null) {
copyBytesToFile(themeValue.byteInputStream(), to) {
theme.value = null
}
}
}
SectionItemView({
val overrides = ThemeManager.currentThemeOverridesForExport(perChat, perUser)
val lines = yaml.encodeToString<ThemeOverrides>(overrides).lines()
// Removing theme id without using custom serializer or data class
theme.value = lines.subList(1, lines.size).joinToString("\n")
withLongRunningApi { exportThemeLauncher.launch("simplex.theme") }
}) {
Text(generalGetString(MR.strings.export_theme), color = colors.primary)
}
val importThemeLauncher = rememberFileChooserLauncher(true) { to: URI? ->
if (to != null) {
val theme = getThemeFromUri(to)
if (theme != null) {
val res = ThemeModeOverride(mode = theme.base.mode, colors = theme.colors, wallpaper = theme.wallpaper?.importFromString()).removeSameColors(theme.base)
save(res)
}
}
}
// Can not limit to YAML mime type since it's unsupported by Android
SectionItemView({ withLongRunningApi { importThemeLauncher.launch("*/*") } }) {
Text(generalGetString(MR.strings.import_theme), color = colors.primary)
}
}
}
@Composable
private fun ResetToGlobalThemeButton(app: Boolean, onClick: () -> Unit) {
SectionItemView(onClick) {
Text(stringResource(if (app) MR.strings.chat_theme_reset_to_app_theme else MR.strings.chat_theme_reset_to_user_theme), color = MaterialTheme.colors.primary)
}
}
@Composable
private fun SetDefaultThemeButton(onClick: () -> Unit) {
SectionItemView(onClick) {
Text(stringResource(MR.strings.chat_theme_set_default_theme), color = MaterialTheme.colors.primary)
}
}
@Composable
private fun AdvancedSettingsButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_arrow_downward),
stringResource(MR.strings.wallpaper_advanced_settings),
click = onClick
)
}
@@ -19,6 +19,7 @@ import kotlinx.serialization.encodeToString
import java.io.* import java.io.*
import java.net.URI import java.net.URI
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.concurrent.Executors import java.util.concurrent.Executors
@@ -145,6 +146,7 @@ fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverri
runCatching { runCatching {
return yaml.decodeFromStream<ThemeOverrides>(it!!) return yaml.decodeFromStream<ThemeOverrides>(it!!)
}.onFailure { }.onFailure {
Log.e(TAG, "Error while decoding theme: ${it.stackTraceToString()}")
if (withAlertOnException) { if (withAlertOnException) {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.import_theme_error), title = generalGetString(MR.strings.import_theme_error),
@@ -280,6 +282,38 @@ fun saveFileFromUri(uri: URI, withAlertOnException: Boolean = true): CryptoFile?
} }
} }
fun saveWallpaperFile(uri: URI): String? {
val destFileName = generateNewFileName("wallpaper", "jpg", File(getWallpaperFilePath("")))
val destFile = File(getWallpaperFilePath(destFileName))
try {
val inputStream = uri.inputStream()
Files.copy(inputStream!!, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
} catch (e: Exception) {
Log.e(TAG, "Error saving wallpaper file: ${e.stackTraceToString()}")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), e.stackTraceToString())
return null
}
return destFile.name
}
fun saveWallpaperFile(image: ImageBitmap): String {
val destFileName = generateNewFileName("wallpaper", "jpg", File(getWallpaperFilePath("")))
val destFile = File(getWallpaperFilePath(destFileName))
val dataResized = resizeImageToDataSize(image, false, maxDataSize = 5_000_000)
val output = FileOutputStream(destFile)
dataResized.use {
it.writeTo(output)
}
return destFile.name
}
fun removeWallpaperFile(fileName: String? = null) {
File(getWallpaperFilePath("_")).parentFile.listFiles()?.forEach {
if (it.name == fileName) it.delete()
}
WallpaperType.cachedImages.remove(fileName)
}
fun <T> createTmpFileAndDelete(onCreated: (File) -> T): T { fun <T> createTmpFileAndDelete(onCreated: (File) -> T): T {
val tmpFile = File(tmpDir, UUID.randomUUID().toString()) val tmpFile = File(tmpDir, UUID.randomUUID().toString())
tmpFile.deleteOnExit() tmpFile.deleteOnExit()
@@ -550,10 +584,33 @@ fun KeyChangeEffect(
val initialKey = remember { key1 } val initialKey = remember { key1 }
val initialKey2 = remember { key2 } val initialKey2 = remember { key2 }
var anyChange by remember { mutableStateOf(false) } var anyChange by remember { mutableStateOf(false) }
LaunchedEffect(key1) { LaunchedEffect(key1, key2) {
if (anyChange || key1 != initialKey || key2 != initialKey2) { if (anyChange || key1 != initialKey || key2 != initialKey2) {
block() block()
anyChange = true anyChange = true
} }
} }
} }
/**
* Runs the [block] only after initial value of the [key1], or [key2], or [key3] changes, not after initial launch
* */
@Composable
@NonRestartableComposable
fun KeyChangeEffect(
key1: Any?,
key2: Any?,
key3: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val initialKey = remember { key1 }
val initialKey2 = remember { key2 }
val initialKey3 = remember { key3 }
var anyChange by remember { mutableStateOf(false) }
LaunchedEffect(key1, key2, key3) {
if (anyChange || key1 != initialKey || key2 != initialKey2 || key3 != initialKey3) {
block()
anyChange = true
}
}
}
@@ -34,7 +34,7 @@ fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) {
} else { } else {
val r: LAResult = if (passcode.value == authRequest.password) { val r: LAResult = if (passcode.value == authRequest.password) {
if (authRequest.selfDestruct && sdPassword != null && controller.ctrl == -1L) { if (authRequest.selfDestruct && sdPassword != null && controller.ctrl == -1L) {
initChatControllerAndRunMigrations() initChatControllerOnStart()
} }
LAResult.Success LAResult.Success
} else { } else {
@@ -669,7 +669,7 @@ private suspend fun MutableState<MigrationToState?>.cleanUpOnBack(chatReceiver:
if (state is MigrationToState.ArchiveImportFailed) { if (state is MigrationToState.ArchiveImportFailed) {
// Original database is not exist, nothing is set up correctly for showing to a user yet. Return to clean state // Original database is not exist, nothing is set up correctly for showing to a user yet. Return to clean state
deleteChatDatabaseFilesAndState() deleteChatDatabaseFilesAndState()
initChatControllerAndRunMigrations() initChatControllerOnStart()
} else if (state is MigrationToState.DownloadProgress && state.ctrl != null) { } else if (state is MigrationToState.DownloadProgress && state.ctrl != null) {
stopArchiveDownloading(state.fileId, state.ctrl) stopArchiveDownloading(state.fileId, state.ctrl)
} }
@@ -144,7 +144,7 @@ fun SettingsLayout(
SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView() }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView() }, disabled = stopped, extraPadding = true)
SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true)
SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true)
SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it, showSettingsModal) }, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it) }, extraPadding = true)
DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
} }
SectionDividerSpaced() SectionDividerSpaced()
@@ -847,7 +847,6 @@
<string name="auth_open_chat_profiles">افتح ملفات تعريف الدردشة</string> <string name="auth_open_chat_profiles">افتح ملفات تعريف الدردشة</string>
<string name="revoke_file__confirm">اسحب الوصول</string> <string name="revoke_file__confirm">اسحب الوصول</string>
<string name="save_archive">حفظ الأرشيف</string> <string name="save_archive">حفظ الأرشيف</string>
<string name="save_color">حفظ اللون</string>
<string name="reveal_verb">كشف</string> <string name="reveal_verb">كشف</string>
<string name="stop_rcv_file__message">سيتم إيقاف استلام الملف.</string> <string name="stop_rcv_file__message">سيتم إيقاف استلام الملف.</string>
<string name="reject_contact_button">رفض</string> <string name="reject_contact_button">رفض</string>
@@ -746,7 +746,7 @@
<string name="private_routing_explanation">To protect your IP address, private routing uses your SMP servers to deliver messages.</string> <string name="private_routing_explanation">To protect your IP address, private routing uses your SMP servers to deliver messages.</string>
<string name="appearance_settings">Appearance</string> <string name="appearance_settings">Appearance</string>
<string name="customize_theme_title">Customize theme</string> <string name="customize_theme_title">Customize theme</string>
<string name="theme_colors_section_title">THEME COLORS</string> <string name="theme_colors_section_title">INTERFACE COLORS</string>
<string name="app_version_title">App version</string> <string name="app_version_title">App version</string>
<string name="app_version_name">App version: v%s</string> <string name="app_version_name">App version: v%s</string>
<string name="app_version_code">App build: %s</string> <string name="app_version_code">App build: %s</string>
@@ -1072,6 +1072,9 @@
<string name="settings_section_title_icon">APP ICON</string> <string name="settings_section_title_icon">APP ICON</string>
<string name="settings_section_title_themes">THEMES</string> <string name="settings_section_title_themes">THEMES</string>
<string name="settings_section_title_profile_images">Profile images</string> <string name="settings_section_title_profile_images">Profile images</string>
<string name="settings_section_title_chat_theme">Chat theme</string>
<string name="settings_section_title_user_theme">Profile theme</string>
<string name="settings_section_title_chat_colors">Chat colors</string>
<string name="settings_section_title_messages">MESSAGES AND FILES</string> <string name="settings_section_title_messages">MESSAGES AND FILES</string>
<string name="settings_section_title_private_message_routing">PRIVATE MESSAGE ROUTING</string> <string name="settings_section_title_private_message_routing">PRIVATE MESSAGE ROUTING</string>
<string name="settings_section_title_calls">CALLS</string> <string name="settings_section_title_calls">CALLS</string>
@@ -1201,6 +1204,7 @@
<string name="incompatible_database_version">Incompatible database version</string> <string name="incompatible_database_version">Incompatible database version</string>
<string name="confirm_database_upgrades">Confirm database upgrades</string> <string name="confirm_database_upgrades">Confirm database upgrades</string>
<string name="terminal_always_visible">Show console in new window</string> <string name="terminal_always_visible">Show console in new window</string>
<string name="chat_list_always_visible">Show chat list in new window</string>
<string name="invalid_migration_confirmation">Invalid migration confirmation</string> <string name="invalid_migration_confirmation">Invalid migration confirmation</string>
<string name="upgrade_and_open_chat">Upgrade and open chat</string> <string name="upgrade_and_open_chat">Upgrade and open chat</string>
<string name="downgrade_and_open_chat">Downgrade and open chat</string> <string name="downgrade_and_open_chat">Downgrade and open chat</string>
@@ -1543,23 +1547,30 @@
<string name="incognito_info_share">When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</string> <string name="incognito_info_share">When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</string>
<!-- Default themes --> <!-- Default themes -->
<string name="color_mode_system">System</string>
<string name="color_mode_light">Light</string>
<string name="color_mode_dark">Dark</string>
<string name="theme_system">System</string> <string name="theme_system">System</string>
<string name="theme_light">Light</string> <string name="theme_light">Light</string>
<string name="theme_dark">Dark</string> <string name="theme_dark">Dark</string>
<string name="theme_simplex">SimpleX</string> <string name="theme_simplex">SimpleX</string>
<string name="theme_black">Black</string>
<!-- Languages --> <!-- Languages -->
<string name="language_system">System</string> <string name="language_system">System</string>
<!-- Appearance.kt --> <!-- Appearance.kt -->
<string name="theme">Theme</string> <string name="theme">Theme</string>
<string name="color_mode">Color mode</string>
<string name="dark_theme">Dark theme</string> <string name="dark_theme">Dark theme</string>
<string name="save_color">Save color</string> <string name="dark_mode_colors">Dark mode colors</string>
<string name="import_theme">Import theme</string> <string name="import_theme">Import theme</string>
<string name="import_theme_error">Import theme error</string> <string name="import_theme_error">Import theme error</string>
<string name="import_theme_error_desc">Make sure the file has correct YAML syntax. Export theme to have an example of the theme file structure.</string> <string name="import_theme_error_desc">Make sure the file has correct YAML syntax. Export theme to have an example of the theme file structure.</string>
<string name="export_theme">Export theme</string> <string name="export_theme">Export theme</string>
<string name="reset_color">Reset colors</string> <string name="reset_color">Reset colors</string>
<string name="reset_single_color">Reset color</string>
<string name="theme_destination_app_theme">App theme</string>
<string name="color_primary">Accent</string> <string name="color_primary">Accent</string>
<string name="color_primary_variant">Additional accent</string> <string name="color_primary_variant">Additional accent</string>
<string name="color_secondary">Secondary</string> <string name="color_secondary">Secondary</string>
@@ -1567,8 +1578,37 @@
<string name="color_background">Background</string> <string name="color_background">Background</string>
<string name="color_surface">Menus &amp; alerts</string> <string name="color_surface">Menus &amp; alerts</string>
<string name="color_title">Title</string> <string name="color_title">Title</string>
<string name="color_primary_variant2">Additional accent 2</string>
<string name="color_sent_message">Sent message</string> <string name="color_sent_message">Sent message</string>
<string name="color_sent_quote">Sent reply</string>
<string name="color_received_message">Received message</string> <string name="color_received_message">Received message</string>
<string name="color_received_quote">Received reply</string>
<string name="color_wallpaper_background">Wallpaper background</string>
<string name="color_wallpaper_tint">Wallpaper accent</string>
<string name="theme_remove_image">Remove image</string>
<!-- Backgrounds -->
<string name="wallpaper_cats">Cats</string>
<string name="wallpaper_flowers">Flowers</string>
<string name="wallpaper_hearts">Hearts</string>
<string name="wallpaper_kids">Kids</string>
<string name="wallpaper_school">School</string>
<string name="wallpaper_travel">Travel</string>
<string name="wallpaper_preview_hello_alice">Good afternoon!</string>
<string name="wallpaper_preview_hello_bob">Good morning!</string>
<string name="wallpaper_scale">Scale</string>
<string name="wallpaper_scale_repeat">Repeat</string>
<string name="wallpaper_scale_fill">Fill</string>
<string name="wallpaper_scale_fit">Fit</string>
<string name="wallpaper_advanced_settings">Advanced settings</string>
<string name="chat_theme_reset_to_app_theme">Reset to app theme</string>
<string name="chat_theme_reset_to_user_theme">Reset to user theme</string>
<string name="chat_theme_set_default_theme">Set default theme</string>
<string name="chat_theme_apply_to_mode">Apply to</string>
<string name="chat_theme_apply_to_all_modes">All color modes</string>
<string name="chat_theme_apply_to_light_mode">Light mode</string>
<string name="chat_theme_apply_to_dark_mode">Dark mode</string>
<!-- Preferences.kt --> <!-- Preferences.kt -->
<string name="chat_preferences_you_allow">You allow</string> <string name="chat_preferences_you_allow">You allow</string>
@@ -1150,7 +1150,6 @@
<string name="network_options_revert">Отмени промените</string> <string name="network_options_revert">Отмени промените</string>
<string name="network_options_save">Запази</string> <string name="network_options_save">Запази</string>
<string name="reset_color">Нулирай цветовете</string> <string name="reset_color">Нулирай цветовете</string>
<string name="save_color">Запази цвета</string>
<string name="color_secondary">Вторичен</string> <string name="color_secondary">Вторичен</string>
<string name="custom_time_picker_select">Избери</string> <string name="custom_time_picker_select">Избери</string>
<string name="to_start_a_new_chat_help_header">За да започнете нов чат</string> <string name="to_start_a_new_chat_help_header">За да започнете нов чат</string>
@@ -55,7 +55,6 @@
<string name="update_network_settings_question">Aktualizovat nastavení sítě\?</string> <string name="update_network_settings_question">Aktualizovat nastavení sítě\?</string>
<string name="incognito">Inkognito</string> <string name="incognito">Inkognito</string>
<string name="incognito_random_profile">Váš náhodný profil</string> <string name="incognito_random_profile">Váš náhodný profil</string>
<string name="save_color">Uložit barvu</string>
<string name="reset_color">Obnovit barvu</string> <string name="reset_color">Obnovit barvu</string>
<string name="color_primary">Zbarvení</string> <string name="color_primary">Zbarvení</string>
<string name="chat_preferences_you_allow">Povolujete</string> <string name="chat_preferences_you_allow">Povolujete</string>
@@ -842,7 +842,6 @@
<string name="theme_dark">Dunkel</string> <string name="theme_dark">Dunkel</string>
<!-- Appearance.kt --> <!-- Appearance.kt -->
<string name="theme">Design</string> <string name="theme">Design</string>
<string name="save_color">Farbe speichern</string>
<string name="reset_color">Farben zurücksetzen</string> <string name="reset_color">Farben zurücksetzen</string>
<string name="color_primary">Akzent</string> <string name="color_primary">Akzent</string>
<!-- Preferences.kt --> <!-- Preferences.kt -->
@@ -607,7 +607,6 @@
<string name="secret_text">secreto</string> <string name="secret_text">secreto</string>
<string name="open_simplex_chat_to_accept_call">Abrir SimpleX Chat para aceptar llamada</string> <string name="open_simplex_chat_to_accept_call">Abrir SimpleX Chat para aceptar llamada</string>
<string name="network_options_reset_to_defaults">Restablecer valores por defecto</string> <string name="network_options_reset_to_defaults">Restablecer valores por defecto</string>
<string name="save_color">Guardar color</string>
<string name="icon_descr_server_status_pending">Pendiente</string> <string name="icon_descr_server_status_pending">Pendiente</string>
<string name="periodic_notifications">Notificaciones periódicas</string> <string name="periodic_notifications">Notificaciones periódicas</string>
<string name="store_passphrase_securely">Guarda la contraseña de forma segura, NO podrás cambiarla si la pierdes.</string> <string name="store_passphrase_securely">Guarda la contraseña de forma segura, NO podrás cambiarla si la pierdes.</string>
@@ -873,7 +873,6 @@
<string name="v4_6_chinese_spanish_interface_descr">Kiitos käyttäjille osallistu Weblaten kautta!</string> <string name="v4_6_chinese_spanish_interface_descr">Kiitos käyttäjille osallistu Weblaten kautta!</string>
<string name="profile_password">Profiilin salasana</string> <string name="profile_password">Profiilin salasana</string>
<string name="reset_color">Oletusvärit</string> <string name="reset_color">Oletusvärit</string>
<string name="save_color">Tallenna väri</string>
<string name="prohibit_sending_voice_messages">Estä ääniviestien lähettäminen.</string> <string name="prohibit_sending_voice_messages">Estä ääniviestien lähettäminen.</string>
<string name="v4_4_disappearing_messages_desc">Lähetetyt viestit poistetaan asetetun ajan kuluttua.</string> <string name="v4_4_disappearing_messages_desc">Lähetetyt viestit poistetaan asetetun ajan kuluttua.</string>
<string name="add_contact_or_create_group">Aloita uusi keskustelu</string> <string name="add_contact_or_create_group">Aloita uusi keskustelu</string>
@@ -783,7 +783,6 @@
<string name="theme_light">Clair</string> <string name="theme_light">Clair</string>
<string name="theme_dark">Sombre</string> <string name="theme_dark">Sombre</string>
<string name="theme">Thème</string> <string name="theme">Thème</string>
<string name="save_color">Enregistrer la couleur</string>
<string name="reset_color">Réinitialisation des couleurs</string> <string name="reset_color">Réinitialisation des couleurs</string>
<string name="color_primary">Principale</string> <string name="color_primary">Principale</string>
<string name="chat_preferences_you_allow">Vous autorisez</string> <string name="chat_preferences_you_allow">Vous autorisez</string>
@@ -69,7 +69,6 @@
<string name="personal_welcome">स्वागत %1$s!</string> <string name="personal_welcome">स्वागत %1$s!</string>
<string name="callstate_starting">शुरुआत</string> <string name="callstate_starting">शुरुआत</string>
<string name="send_verb">भेजना</string> <string name="send_verb">भेजना</string>
<string name="save_color">रंग बचाओ</string>
<string name="share_verb">साझा करना</string> <string name="share_verb">साझा करना</string>
<string name="reject_contact_button">अस्वीकार</string> <string name="reject_contact_button">अस्वीकार</string>
<string name="network_use_onion_hosts_required">आवश्यक</string> <string name="network_use_onion_hosts_required">आवश्यक</string>
@@ -1196,7 +1196,6 @@
<string name="scan_code">Kód beolvasása</string> <string name="scan_code">Kód beolvasása</string>
<string name="open_port_in_firewall_title">Port megnyitása a tűzfalon</string> <string name="open_port_in_firewall_title">Port megnyitása a tűzfalon</string>
<string name="callstate_starting">indítás…</string> <string name="callstate_starting">indítás…</string>
<string name="save_color">Szín mentése</string>
<string name="settings_shutdown">Leállítás</string> <string name="settings_shutdown">Leállítás</string>
<string name="icon_descr_sent_msg_status_sent">elküldve</string> <string name="icon_descr_sent_msg_status_sent">elküldve</string>
<string name="network_socks_toggle_use_socks_proxy">SOCKS proxy használata</string> <string name="network_socks_toggle_use_socks_proxy">SOCKS proxy használata</string>
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

@@ -847,7 +847,6 @@
<string name="prohibit_sending_voice_messages">Proibisci l\'invio di messaggi vocali.</string> <string name="prohibit_sending_voice_messages">Proibisci l\'invio di messaggi vocali.</string>
<string name="feature_received_prohibited">ricevuto, vietato</string> <string name="feature_received_prohibited">ricevuto, vietato</string>
<string name="reset_color">Ripristina i colori</string> <string name="reset_color">Ripristina i colori</string>
<string name="save_color">Salva colore</string>
<string name="accept_feature_set_1_day">Imposta 1 giorno</string> <string name="accept_feature_set_1_day">Imposta 1 giorno</string>
<string name="set_group_preferences">Imposta le preferenze del gruppo</string> <string name="set_group_preferences">Imposta le preferenze del gruppo</string>
<string name="theme">Tema</string> <string name="theme">Tema</string>
@@ -898,7 +898,6 @@
<string name="save_passphrase_and_open_chat">שמור סיסמה ופתח את הצ׳אט</string> <string name="save_passphrase_and_open_chat">שמור סיסמה ופתח את הצ׳אט</string>
<string name="save_archive">שמור ארכיון</string> <string name="save_archive">שמור ארכיון</string>
<string name="select_contacts">בחירת אנשי קשר</string> <string name="select_contacts">בחירת אנשי קשר</string>
<string name="save_color">שמור צבע</string>
<string name="v5_1_self_destruct_passcode">קוד גישה להשמדה עצמית</string> <string name="v5_1_self_destruct_passcode">קוד גישה להשמדה עצמית</string>
<string name="custom_time_unit_seconds">שניות</string> <string name="custom_time_unit_seconds">שניות</string>
<string name="custom_time_picker_select">אישור</string> <string name="custom_time_picker_select">אישור</string>
@@ -792,7 +792,6 @@
<string name="network_options_save">保存</string> <string name="network_options_save">保存</string>
<string name="update_network_settings_question">ネットワーク設定を更新しますか?</string> <string name="update_network_settings_question">ネットワーク設定を更新しますか?</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">設定を更新すると、全サーバにクライントの再接続が行われます。</string> <string name="updating_settings_will_reconnect_client_to_all_servers">設定を更新すると、全サーバにクライントの再接続が行われます。</string>
<string name="save_color">色を保存</string>
<string name="chat_preferences_you_allow">あなたが次を許可しています:</string> <string name="chat_preferences_you_allow">あなたが次を許可しています:</string>
<string name="chat_preferences_yes">オン</string> <string name="chat_preferences_yes">オン</string>
<string name="set_group_preferences">グループ設定を行う</string> <string name="set_group_preferences">グループ設定を行う</string>
@@ -757,7 +757,6 @@
<string name="save_and_notify_contact">저장하고 대화 상대에게 알리기</string> <string name="save_and_notify_contact">저장하고 대화 상대에게 알리기</string>
<string name="remove_passphrase">지우기</string> <string name="remove_passphrase">지우기</string>
<string name="save_archive">아카이브 저장하기</string> <string name="save_archive">아카이브 저장하기</string>
<string name="save_color">색상 저장하기</string>
<string name="save_passphrase_in_keychain">암호 저장소에 비밀번호 저장하기</string> <string name="save_passphrase_in_keychain">암호 저장소에 비밀번호 저장하기</string>
<string name="restore_database">데이터베이스 백업 복원하기</string> <string name="restore_database">데이터베이스 백업 복원하기</string>
<string name="restore_database_alert_desc">데이터베이스 백업을 복원한 후 이전 비밀번호를 입력해 주세요. 이 작업은 되돌릴 수 없어요.</string> <string name="restore_database_alert_desc">데이터베이스 백업을 복원한 후 이전 비밀번호를 입력해 주세요. 이 작업은 되돌릴 수 없어요.</string>
@@ -189,7 +189,6 @@
<string name="dont_show_again">Daugiau neberodyti</string> <string name="dont_show_again">Daugiau neberodyti</string>
<string name="theme_dark">Tamsus</string> <string name="theme_dark">Tamsus</string>
<string name="reset_color">Atstatyti spalvas</string> <string name="reset_color">Atstatyti spalvas</string>
<string name="save_color">Įrašyti spalvą</string>
<string name="group_preferences">Grupės parinktys</string> <string name="group_preferences">Grupės parinktys</string>
<string name="full_deletion">Ištrinti visiems</string> <string name="full_deletion">Ištrinti visiems</string>
<string name="direct_messages">Tiesioginės žinutės</string> <string name="direct_messages">Tiesioginės žinutės</string>
@@ -323,7 +323,6 @@
<string name="network_options_revert">പഴയപടിയാക്കുക</string> <string name="network_options_revert">പഴയപടിയാക്കുക</string>
<string name="theme_system">സംവിധാനം</string> <string name="theme_system">സംവിധാനം</string>
<string name="language_system">സംവിധാനം</string> <string name="language_system">സംവിധാനം</string>
<string name="save_color">നിറം സംരക്ഷിക്കുക</string>
<string name="color_title">ശീർഷകം</string> <string name="color_title">ശീർഷകം</string>
<string name="color_secondary">രണ്ടാംതരമായ</string> <string name="color_secondary">രണ്ടാംതരമായ</string>
<string name="chat_preferences_yes">അതെ</string> <string name="chat_preferences_yes">അതെ</string>
@@ -814,7 +814,6 @@
<string name="incognito_info_share">Wanneer je een incognito profiel met iemand deelt, wordt dit profiel gebruikt voor de groepen waarvoor ze je uitnodigen.</string> <string name="incognito_info_share">Wanneer je een incognito profiel met iemand deelt, wordt dit profiel gebruikt voor de groepen waarvoor ze je uitnodigen.</string>
<string name="theme">Thema</string> <string name="theme">Thema</string>
<string name="reset_color">Kleuren resetten</string> <string name="reset_color">Kleuren resetten</string>
<string name="save_color">Kleur opslaan</string>
<string name="theme_system">Systeem</string> <string name="theme_system">Systeem</string>
<string name="chat_preferences_yes">ja</string> <string name="chat_preferences_yes">ja</string>
<string name="feature_received_prohibited">gekregen, verboden</string> <string name="feature_received_prohibited">gekregen, verboden</string>
@@ -799,7 +799,6 @@
<string name="prohibit_sending_disappearing_messages">Zabroń wysyłania znikających wiadomości.</string> <string name="prohibit_sending_disappearing_messages">Zabroń wysyłania znikających wiadomości.</string>
<string name="feature_received_prohibited">otrzymane, zabronione</string> <string name="feature_received_prohibited">otrzymane, zabronione</string>
<string name="reset_color">Resetuj kolory</string> <string name="reset_color">Resetuj kolory</string>
<string name="save_color">Zapisz kolor</string>
<string name="accept_feature_set_1_day">Ustaw 1 dzień</string> <string name="accept_feature_set_1_day">Ustaw 1 dzień</string>
<string name="theme_system">System</string> <string name="theme_system">System</string>
<string name="language_system">System</string> <string name="language_system">System</string>
@@ -485,7 +485,6 @@
<string name="network_options_revert">Reverter</string> <string name="network_options_revert">Reverter</string>
<string name="network_options_save">Salvar</string> <string name="network_options_save">Salvar</string>
<string name="reset_color">Redefinir cores</string> <string name="reset_color">Redefinir cores</string>
<string name="save_color">Salvar cor</string>
<string name="v4_5_italian_interface">interface italiana</string> <string name="v4_5_italian_interface">interface italiana</string>
<string name="periodic_notifications">Notificações periódicas</string> <string name="periodic_notifications">Notificações periódicas</string>
<string name="use_camera_button">Câmera</string> <string name="use_camera_button">Câmera</string>
@@ -370,7 +370,6 @@
<string name="invite_prohibited_description">Você está a tentar convidar um contato com quem partilhou um perfil anónimo para o grupo no qual voçê está a usar o seu perfil principal</string> <string name="invite_prohibited_description">Você está a tentar convidar um contato com quem partilhou um perfil anónimo para o grupo no qual voçê está a usar o seu perfil principal</string>
<string name="info_row_connection">Conexão</string> <string name="info_row_connection">Conexão</string>
<string name="incognito_info_protects">O modo anónimo protege a privacidade do nome e da imagem do seu perfil principal — para cada novo contato um novo perfil aleatório é criado.</string> <string name="incognito_info_protects">O modo anónimo protege a privacidade do nome e da imagem do seu perfil principal — para cada novo contato um novo perfil aleatório é criado.</string>
<string name="save_color">Salvar cor</string>
<string name="description_you_shared_one_time_link">você partilhou ligação de utilização única</string> <string name="description_you_shared_one_time_link">você partilhou ligação de utilização única</string>
<string name="description_you_shared_one_time_link_incognito">você partilhou ligação anónima de utilização única</string> <string name="description_you_shared_one_time_link_incognito">você partilhou ligação anónima de utilização única</string>
<string name="invalid_connection_link">Ligação de conexão inválida</string> <string name="invalid_connection_link">Ligação de conexão inválida</string>
@@ -848,7 +848,6 @@
<string name="theme_dark">Темная</string> <string name="theme_dark">Темная</string>
<!-- Appearance.kt --> <!-- Appearance.kt -->
<string name="theme">Тема</string> <string name="theme">Тема</string>
<string name="save_color">Сохранить цвет</string>
<string name="reset_color">Сбросить цвета</string> <string name="reset_color">Сбросить цвета</string>
<string name="color_primary">Акцент</string> <string name="color_primary">Акцент</string>
<!-- Preferences.kt --> <!-- Preferences.kt -->
@@ -855,7 +855,6 @@
<string name="network_options_revert">เปลี่ยนกลับ</string> <string name="network_options_revert">เปลี่ยนกลับ</string>
<string name="network_options_save">บันทึก</string> <string name="network_options_save">บันทึก</string>
<string name="reset_color">รีเซ็ตสี</string> <string name="reset_color">รีเซ็ตสี</string>
<string name="save_color">บันทึกสี</string>
<string name="feature_received_prohibited">ได้รับ, ห้าม</string> <string name="feature_received_prohibited">ได้รับ, ห้าม</string>
<string name="v4_4_live_messages_desc">ผู้รับจะเห็นการอัปเดตเมื่อคุณพิมพ์</string> <string name="v4_4_live_messages_desc">ผู้รับจะเห็นการอัปเดตเมื่อคุณพิมพ์</string>
<string name="v4_5_reduced_battery_usage">ลดการใช้แบตเตอรี่</string> <string name="v4_5_reduced_battery_usage">ลดการใช้แบตเตอรี่</string>
@@ -122,7 +122,6 @@
<string name="theme_simplex">SimpleX</string> <string name="theme_simplex">SimpleX</string>
<string name="dark_theme">Koyu tema</string> <string name="dark_theme">Koyu tema</string>
<string name="theme">Tema</string> <string name="theme">Tema</string>
<string name="save_color">Rengi kaydet</string>
<string name="import_theme">Temayı içe aktar</string> <string name="import_theme">Temayı içe aktar</string>
<string name="import_theme_error">Temayı içe aktarırken hata oluştu</string> <string name="import_theme_error">Temayı içe aktarırken hata oluştu</string>
<string name="import_theme_error_desc">Dosyanın doğru YAML sözdizimine sahip olduğundan emin olun. Tema dosyası yapısının bir örneğine sahip olmak için temayı dışa aktarın.</string> <string name="import_theme_error_desc">Dosyanın doğru YAML sözdizimine sahip olduğundan emin olun. Tema dosyası yapısının bir örneğine sahip olmak için temayı dışa aktarın.</string>
@@ -450,7 +450,6 @@
<string name="change_member_role_question">Змінити роль у групі\?</string> <string name="change_member_role_question">Змінити роль у групі\?</string>
<string name="error_removing_member">Помилка при вилученні учасника</string> <string name="error_removing_member">Помилка при вилученні учасника</string>
<string name="group_main_profile_sent">Ваш профіль чату буде відправлений учасникам групи</string> <string name="group_main_profile_sent">Ваш профіль чату буде відправлений учасникам групи</string>
<string name="save_color">Зберегти колір</string>
<string name="full_deletion">Видалення для всіх</string> <string name="full_deletion">Видалення для всіх</string>
<string name="voice_messages">Голосові повідомлення</string> <string name="voice_messages">Голосові повідомлення</string>
<string name="voice_prohibited_in_this_chat">Голосові повідомлення заборонені в цьому чаті.</string> <string name="voice_prohibited_in_this_chat">Голосові повідомлення заборонені в цьому чаті.</string>
@@ -831,7 +831,6 @@
<string name="network_option_seconds_label"></string> <string name="network_option_seconds_label"></string>
<string name="network_options_revert">恢复</string> <string name="network_options_revert">恢复</string>
<string name="reset_color">重置颜色</string> <string name="reset_color">重置颜色</string>
<string name="save_color">保存颜色</string>
<string name="v4_5_reduced_battery_usage">减少电池使用量</string> <string name="v4_5_reduced_battery_usage">减少电池使用量</string>
<string name="v4_5_private_filenames_descr">为了保护时区,图像/语音文件使用 UTC。</string> <string name="v4_5_private_filenames_descr">为了保护时区,图像/语音文件使用 UTC。</string>
<string name="use_chat">使用聊天</string> <string name="use_chat">使用聊天</string>
@@ -827,7 +827,6 @@
<string name="error_saving_group_profile">儲存群組檔案時出錯</string> <string name="error_saving_group_profile">儲存群組檔案時出錯</string>
<string name="network_options_revert">恢復</string> <string name="network_options_revert">恢復</string>
<string name="theme">主題</string> <string name="theme">主題</string>
<string name="save_color">儲存顏色</string>
<string name="chat_preferences_you_allow">你允許</string> <string name="chat_preferences_you_allow">你允許</string>
<string name="set_group_preferences">修改群組內的設定</string> <string name="set_group_preferences">修改群組內的設定</string>
<string name="direct_messages">私訊</string> <string name="direct_messages">私訊</string>
@@ -0,0 +1,38 @@
package chat.simplex.app
import chat.simplex.common.ui.theme.*
import kotlin.test.Test
import kotlin.test.assertEquals
// use this command for testing:
// ./gradlew desktopTest
class ThemesTest {
@Test
fun testSkipDuplicates() {
val r = ArrayList<ThemeOverrides>()
r.add(ThemeOverrides("UUID", DefaultTheme.DARK))
r.add(ThemeOverrides("UUID", DefaultTheme.DARK))
r.add(ThemeOverrides("UUID", DefaultTheme.LIGHT))
r.add(ThemeOverrides("UUID2", DefaultTheme.DARK))
r.add(ThemeOverrides("UUID3", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper()))
r.add(ThemeOverrides("UUID4", DefaultTheme.LIGHT, wallpaper = null))
r.add(ThemeOverrides("UUID5", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(preset = "something")))
r.add(ThemeOverrides("UUID5", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(preset = "something2")))
r.add(ThemeOverrides("UUID6", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(preset = "something2")))
r.add(ThemeOverrides("UUID7", DefaultTheme.DARK, wallpaper = ThemeWallpaper(preset = "something2")))
r.add(ThemeOverrides("UUID8", DefaultTheme.DARK, wallpaper = ThemeWallpaper(imageFile = "image")))
r.add(ThemeOverrides("UUID9", DefaultTheme.DARK, wallpaper = ThemeWallpaper(imageFile = "image2")))
r.add(ThemeOverrides("UUID10", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(imageFile = "image")))
assertEquals(
r.skipDuplicates(), listOf(
ThemeOverrides("UUID", DefaultTheme.DARK),
ThemeOverrides("UUID3", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper()),
ThemeOverrides("UUID5", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(preset = "something")),
ThemeOverrides("UUID6", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(preset = "something2")),
ThemeOverrides("UUID7", DefaultTheme.DARK, wallpaper = ThemeWallpaper(preset = "something2")),
ThemeOverrides("UUID8", DefaultTheme.DARK, wallpaper = ThemeWallpaper(imageFile = "image")),
ThemeOverrides("UUID10", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(imageFile = "image"))
)
)
}
}
@@ -4,8 +4,7 @@ import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme import androidx.compose.material.*
import androidx.compose.material.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -26,7 +26,7 @@ fun initApp() {
} }
applyAppLocale() applyAppLocale()
if (DatabaseUtils.ksSelfDestructPassword.get() == null) { if (DatabaseUtils.ksSelfDestructPassword.get() == null) {
initChatControllerAndRunMigrations() initChatControllerOnStart()
} }
// LALAL // LALAL
//testCrypto() //testCrypto()
@@ -13,8 +13,10 @@ actual val dataDir: File = File(desktopPlatform.dataPath)
actual val tmpDir: File = File(System.getProperty("java.io.tmpdir") + File.separator + "simplex").also { it.deleteOnExit() } actual val tmpDir: File = File(System.getProperty("java.io.tmpdir") + File.separator + "simplex").also { it.deleteOnExit() }
actual val filesDir: File = File(dataDir.absolutePath + File.separator + "simplex_v1_files") actual val filesDir: File = File(dataDir.absolutePath + File.separator + "simplex_v1_files")
actual val appFilesDir: File = filesDir actual val appFilesDir: File = filesDir
actual val wallpapersDir: File = File(dataDir.absolutePath + File.separator + "simplex_v1_assets" + File.separator + "wallpapers").also { it.mkdirs() }
actual val coreTmpDir: File = File(dataDir.absolutePath + File.separator + "tmp") actual val coreTmpDir: File = File(dataDir.absolutePath + File.separator + "tmp")
actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "simplex_v1" actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "simplex_v1"
actual val preferencesDir = File(desktopPlatform.configPath).also { it.parentFile.mkdirs() }
actual val chatDatabaseFileName: String = "simplex_v1_chat.db" actual val chatDatabaseFileName: String = "simplex_v1_chat.db"
actual val agentDatabaseFileName: String = "simplex_v1_agent.db" actual val agentDatabaseFileName: String = "simplex_v1_agent.db"
@@ -1,12 +1,17 @@
package chat.simplex.common.platform package chat.simplex.common.platform
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import chat.simplex.common.simplexWindowState import chat.simplex.common.simplexWindowState
import chat.simplex.common.ui.theme.reactOnDarkThemeChanges
import com.jthemedetecor.OsThemeDetector
import com.russhwolf.settings.* import com.russhwolf.settings.*
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.desc.desc import dev.icerock.moko.resources.desc.desc
import java.io.File import java.io.File
@@ -18,7 +23,15 @@ actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle)
actual fun StringResource.localized(): String = desc().toString() actual fun StringResource.localized(): String = desc().toString()
actual fun isInNightMode() = false private val detector: OsThemeDetector = OsThemeDetector.getDetector()
actual fun isInNightMode() = try {
detector.isDark
}
catch (e: Exception) {
Log.e(TAG, e.stackTraceToString())
/* On Mac this code can produce exception */
false
}
private val settingsFile = private val settingsFile =
File(desktopPlatform.configPath + File.separator + "settings.properties") File(desktopPlatform.configPath + File.separator + "settings.properties")
@@ -58,3 +71,6 @@ actual fun isRtl(text: CharSequence): Boolean {
dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT || dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT || dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC
} }
} }
actual fun ImageResource.toComposeImageBitmap(): ImageBitmap? =
image.toComposeImageBitmap()
@@ -10,6 +10,10 @@ private val detector: OsThemeDetector = OsThemeDetector.getDetector()
registerListener(::reactOnDarkThemeChanges) registerListener(::reactOnDarkThemeChanges)
} }
// TODO: explore possibility to use
//@Composable
//actual fun isSystemInDarkTheme(): Boolean = androidx.compose.foundation.isSystemInDarkTheme()
@Composable @Composable
actual fun isSystemInDarkTheme(): Boolean = try { actual fun isSystemInDarkTheme(): Boolean = try {
detector.isDark detector.isDark
@@ -25,11 +25,6 @@ val connections = ArrayList<WebSocket>()
@Composable @Composable
actual fun ActiveCallView() { actual fun ActiveCallView() {
val endCall = {
val call = chatModel.activeCall.value
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
}
BackHandler(onBack = endCall)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
WebRTCController(chatModel.callCommand) { apiMsg -> WebRTCController(chatModel.callCommand) { apiMsg ->
Log.d(TAG, "received from WebRTCController: $apiMsg") Log.d(TAG, "received from WebRTCController: $apiMsg")
@@ -4,34 +4,23 @@ import SectionBottomSpacer
import SectionDividerSpaced import SectionDividerSpaced
import SectionView import SectionView
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.SharedPreference import chat.simplex.common.model.SharedPreference
import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.*
import chat.simplex.common.platform.defaultLocale
import chat.simplex.common.ui.theme.ThemeColor
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.AppearanceScope.ColorEditor
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import java.util.Locale import java.util.Locale
@Composable @Composable
actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) { actual fun AppearanceView(m: ChatModel) {
AppearanceScope.AppearanceLayout( AppearanceScope.AppearanceLayout(
m.controller.appPrefs.appLanguage, m.controller.appPrefs.appLanguage,
m.controller.appPrefs.systemDarkTheme, m.controller.appPrefs.systemDarkTheme,
showSettingsModal = showSettingsModal,
editColor = { name, initialColor ->
ModalManager.start.showModalCloseable { close ->
ColorEditor(name, initialColor, close)
}
},
) )
} }
@@ -39,8 +28,6 @@ actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatMod
fun AppearanceScope.AppearanceLayout( fun AppearanceScope.AppearanceLayout(
languagePref: SharedPreference<String?>, languagePref: SharedPreference<String?>,
systemDarkTheme: SharedPreference<String?>, systemDarkTheme: SharedPreference<String?>,
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
editColor: (ThemeColor, Color) -> Unit,
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
@@ -63,10 +50,11 @@ fun AppearanceScope.AppearanceLayout(
} }
} }
SectionDividerSpaced(maxTopPadding = true) SectionDividerSpaced(maxTopPadding = true)
ProfileImageSection() ThemesSection(systemDarkTheme)
SectionDividerSpaced(maxTopPadding = true) SectionDividerSpaced(maxTopPadding = true)
ThemesSection(systemDarkTheme, showSettingsModal, editColor) ProfileImageSection()
SectionBottomSpacer() SectionBottomSpacer()
} }
} }
@@ -18,6 +18,7 @@ import java.io.File
fun main() { fun main() {
initHaskell() initHaskell()
runMigrations()
initApp() initApp()
tmpDir.deleteRecursively() tmpDir.deleteRecursively()
tmpDir.mkdir() tmpDir.mkdir()
+4 -4
View File
@@ -26,11 +26,11 @@ android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11 kotlin.jvm.target=11
android.version_name=5.8-beta.2 android.version_name=5.8-beta.3
android.version_code=210 android.version_code=214
desktop.version_name=5.8-beta.2 desktop.version_name=5.8-beta.3
desktop.version_code=47 desktop.version_code=49
kotlin.version=1.9.23 kotlin.version=1.9.23
gradle.plugin.version=8.2.0 gradle.plugin.version=8.2.0
@@ -35,8 +35,10 @@ import Simplex.Chat.Messages.CIContent
import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Protocol (MsgContent (..))
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Shared
import Simplex.Messaging.Agent.Protocol (AgentErrorType (..))
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Util ((<$?>)) import Simplex.Messaging.Protocol (BrokerErrorType (..))
import Simplex.Messaging.Util (tshow, (<$?>))
data DirectoryEvent data DirectoryEvent
= DEContactConnected Contact = DEContactConnected Contact
@@ -53,6 +55,7 @@ data DirectoryEvent
| DEItemEditIgnored Contact | DEItemEditIgnored Contact
| DEItemDeleteIgnored Contact | DEItemDeleteIgnored Contact
| DEContactCommand Contact ChatItemId ADirectoryCmd | DEContactCommand Contact ChatItemId ADirectoryCmd
| DELogChatResponse Text
deriving (Show) deriving (Show)
crDirectoryEvent :: ChatResponse -> Maybe DirectoryEvent crDirectoryEvent :: ChatResponse -> Maybe DirectoryEvent
@@ -77,6 +80,13 @@ crDirectoryEvent = \case
where where
ciId = chatItemId' ci ciId = chatItemId' ci
err = ADC SDRUser DCUnknownCommand err = ADC SDRUser DCUnknownCommand
CRMessageError {severity, errorMessage} -> Just $ DELogChatResponse $ "message error: " <> severity <> ", " <> errorMessage
CRChatCmdError {chatError} -> Just $ DELogChatResponse $ "chat cmd error: " <> tshow chatError
CRChatError {chatError} -> case chatError of
ChatErrorAgent {agentError = BROKER _ NETWORK} -> Nothing
ChatErrorAgent {agentError = BROKER _ TIMEOUT} -> Nothing
_ -> Just $ DELogChatResponse $ "chat error: " <> tshow chatError
CRChatErrors {chatErrors} -> Just $ DELogChatResponse $ "chat errors: " <> T.intercalate ", " (map tshow chatErrors)
_ -> Nothing _ -> Nothing
data DirectoryRole = DRUser | DRSuperUser data DirectoryRole = DRUser | DRSuperUser
@@ -102,6 +102,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
case sUser of case sUser of
SDRUser -> deUserCommand env ct ciId cmd SDRUser -> deUserCommand env ct ciId cmd
SDRSuperUser -> deSuperUserCommand ct ciId cmd SDRSuperUser -> deSuperUserCommand ct ciId cmd
DELogChatResponse r -> logInfo r
where where
withSuperUsers action = void . forkIO $ forM_ superUsers $ \KnownContact {contactId} -> action contactId withSuperUsers action = void . forkIO $ forM_ superUsers $ \KnownContact {contactId} -> action contactId
notifySuperUsers s = withSuperUsers $ \contactId -> sendMessage' cc contactId s notifySuperUsers s = withSuperUsers $ \contactId -> sendMessage' cc contactId s
@@ -39,8 +39,8 @@ import Data.Text (Text)
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Util (ifM) import Simplex.Messaging.Util (ifM)
import System.IO (Handle, IOMode (..), openFile, BufferMode (..), hSetBuffering) import System.Directory (doesFileExist, renameFile)
import System.Directory (renameFile, doesFileExist) import System.IO (BufferMode (..), Handle, IOMode (..), hSetBuffering, openFile)
data DirectoryStore = DirectoryStore data DirectoryStore = DirectoryStore
{ groupRegs :: TVar [GroupReg], { groupRegs :: TVar [GroupReg],
@@ -112,7 +112,7 @@ addGroupReg st ct GroupInfo {groupId} grStatus = do
let ugrId = 1 + foldl' maxUgrId 0 grs let ugrId = 1 + foldl' maxUgrId 0 grs
grData' = grData {userGroupRegId_ = ugrId} grData' = grData {userGroupRegId_ = ugrId}
gr' = gr {userGroupRegId = ugrId} gr' = gr {userGroupRegId = ugrId}
in (grData', gr' : grs) in (grData', gr' : grs)
ctId = contactId' ct ctId = contactId' ct
maxUgrId mx GroupReg {dbContactId, userGroupRegId} maxUgrId mx GroupReg {dbContactId, userGroupRegId}
| dbContactId == ctId && userGroupRegId > mx = userGroupRegId | dbContactId == ctId && userGroupRegId > mx = userGroupRegId
@@ -311,14 +311,15 @@ readDirectoryData f =
Right r -> case r of Right r -> case r of
GRCreate gr@GroupRegData {dbGroupId_ = gId} -> do GRCreate gr@GroupRegData {dbGroupId_ = gId} -> do
when (isJust $ M.lookup gId m) $ when (isJust $ M.lookup gId m) $
putStrLn $ "Warning: duplicate group with ID " <> show gId <> ", group replaced." putStrLn $
"Warning: duplicate group with ID " <> show gId <> ", group replaced."
pure $ M.insert gId gr m pure $ M.insert gId gr m
GRUpdateStatus gId groupRegStatus_ -> case M.lookup gId m of GRUpdateStatus gId groupRegStatus_ -> case M.lookup gId m of
Just gr -> pure $ M.insert gId gr {groupRegStatus_} m Just gr -> pure $ M.insert gId gr {groupRegStatus_} m
Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <>", status update ignored.") Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <> ", status update ignored.")
GRUpdateOwner gId grOwnerId -> case M.lookup gId m of GRUpdateOwner gId grOwnerId -> case M.lookup gId m of
Just gr -> pure $ M.insert gId gr {dbOwnerMemberId_ = Just grOwnerId} m Just gr -> pure $ M.insert gId gr {dbOwnerMemberId_ = Just grOwnerId} m
Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <>", owner update ignored.") Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <> ", owner update ignored.")
writeDirectoryData :: FilePath -> [GroupRegData] -> IO Handle writeDirectoryData :: FilePath -> [GroupRegData] -> IO Handle
writeDirectoryData f grs = do writeDirectoryData f grs = do
+1 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package source-repository-package
type: git type: git
location: https://github.com/simplex-chat/simplexmq.git location: https://github.com/simplex-chat/simplexmq.git
tag: e3f5d244c1a435593e33adc023bf1f920f379f8d tag: e2f4ffc9db8e3812cfaf12ded9904061fb330fd6
source-repository-package source-repository-package
type: git type: git
+22 -3
View File
@@ -1,5 +1,5 @@
name: simplex-chat name: simplex-chat
version: 5.8.0.2 version: 5.8.0.3
#synopsis: #synopsis:
#description: #description:
homepage: https://github.com/simplex-chat/simplex-chat#readme homepage: https://github.com/simplex-chat/simplex-chat#readme
@@ -152,12 +152,31 @@ tests:
ghc-options: ghc-options:
# - -haddock # - -haddock
- -O2 - -O2
- -Wall - -Weverything
- -Wno-missing-exported-signatures
- -Wno-missing-import-lists
- -Wno-missed-specialisations
- -Wno-all-missed-specialisations
- -Wno-unsafe
- -Wno-safe
- -Wno-missing-local-signatures
- -Wno-missing-kind-signatures
- -Wno-missing-deriving-strategies
- -Wno-monomorphism-restriction
- -Wno-prepositive-qualified-module
- -Wno-unused-packages
- -Wno-implicit-prelude
- -Wno-missing-safe-haskell-mode
- -Wno-missing-export-lists
- -Wno-partial-fields
- -Wcompat - -Wcompat
- -Werror=incomplete-record-updates
- -Werror=incomplete-patterns - -Werror=incomplete-patterns
- -Werror=missing-methods
- -Werror=incomplete-uni-patterns
- -Werror=tabs
- -Wredundant-constraints - -Wredundant-constraints
- -Wincomplete-record-updates - -Wincomplete-record-updates
- -Wincomplete-uni-patterns
- -Wunused-type-patterns - -Wunused-type-patterns
default-extensions: default-extensions:
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"https://github.com/simplex-chat/simplexmq.git"."e3f5d244c1a435593e33adc023bf1f920f379f8d" = "1klin78kgvgzdvf64nahn3280m7hw5f8wzrca43cmyajm2qp3wfs"; "https://github.com/simplex-chat/simplexmq.git"."e2f4ffc9db8e3812cfaf12ded9904061fb330fd6" = "0mvg8wn8brcw71fh0lsf39lypha2kfil443bq7gz6zhwl87vaz28";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";

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