Compare commits

..

35 Commits

Author SHA1 Message Date
spaced4ndy e67feeea22 operators: debug decoding 2024-11-12 11:02:58 +04:00
Evgeny Poberezkin da65474452 CLI API in progress, validateUserServers 2024-11-11 23:24:11 +00:00
Evgeny Poberezkin d0a7e14a96 make preset servers lists potentially empty in some operators, as long as the combined list is not empty 2024-11-11 16:22:19 +00:00
Evgeny Poberezkin bd4745775d update 2024-11-11 11:34:02 +00:00
Evgeny Poberezkin af144c6208 fix 2024-11-10 22:58:23 +00:00
Evgeny Poberezkin 74206a947b Merge branch 'server-operators' into ep/operators-preset-servers 2024-11-10 16:15:39 +00:00
Evgeny Poberezkin 457e12880c Merge branch 'master' into server-operators 2024-11-10 16:15:07 +00:00
Evgeny 2d588949b1 directory service: additional commands (#5159)
* directory service: additional commands

* notify superusers

* 48 hours

* replace T.elem
2024-11-10 15:21:33 +00:00
Evgeny Poberezkin 84f7f901ea remove imports 2024-11-10 12:57:57 +00:00
Evgeny Poberezkin 90ed503ee0 update (most tests pass) 2024-11-10 12:30:24 +00:00
Evgeny Poberezkin 28105038d4 preset operators and servers (compiles) 2024-11-09 23:53:18 +00:00
Evgeny Poberezkin fd60a2402a Merge branch 'server-operators' into ep/operators-preset-servers 2024-11-08 14:36:47 +00:00
spaced4ndy ef0f21a11c core: operator apis commands (#5155) 2024-11-08 14:45:00 +04:00
spaced4ndy d0e40c1f1d Merge branch 'server-operators' into ep/operators-preset-servers 2024-11-06 16:20:03 +04:00
spaced4ndy 8396e70e7b core: validate servers - find servers with duplicate hosts (#5150) 2024-11-06 16:13:08 +04:00
Evgeny Poberezkin 6128a24869 database entity IDs 2024-11-06 08:12:16 +00:00
Evgeny Poberezkin 0329a6a7d3 Merge branch 'server-operators' into ep/operators-preset-servers 2024-11-05 18:07:09 +00:00
spaced4ndy 2da89c2cf1 core: setConditionsNotified, acceptConditions, setUserServers, validateServers apis wip (#5147) 2024-11-05 21:40:33 +04:00
Evgeny Poberezkin 601ddf97ce WIP 2024-11-05 16:29:30 +00:00
Evgeny Poberezkin 128d031ced Merge branch 'server-operators' into ep/operators-preset-servers 2024-11-05 10:55:36 +00:00
spaced4ndy 3b0205b25f core: setServerOperators, getUsageConditions api wip (#5145) 2024-11-05 14:15:20 +04:00
Evgeny Poberezkin d4a47f1cce simplify 2024-11-04 20:07:28 +00:00
Evgeny Poberezkin 2998e3af3f usageConditionsToAdd 2024-11-04 19:44:45 +00:00
Evgeny Poberezkin 5ff838d63e Merge branch 'server-operators' into ep/operators-preset-servers 2024-11-04 18:19:24 +00:00
spaced4ndy bdaec30fa0 core: getServerOperators, getUserServers, getUsageConditions apis wip (#5141) 2024-11-04 21:11:03 +04:00
Evgeny Poberezkin 5a8bf9106e core: preset servers and operators (WIP) 2024-11-04 16:42:37 +00:00
Evgeny 97df069730 core: add support for server operators (#4961)
* core: add support for server operators

* migration

* update schema and queries, rfc

* add usage conditions tables

* core: server operators new apis draft

* update

* conditions

* update

* add get conditions api

* add get conditions API

* WIP

* compiles

* fix schema

* core: ui logic in types (#5139)

* update

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-11-04 13:28:57 +00:00
Evgeny Poberezkin 9a1166f097 Merge branch 'master' into server-operators 2024-11-03 09:12:12 +00:00
Evgeny Poberezkin 7a741e7ac4 ios: update core library 2024-11-02 20:03:27 +00:00
Alexander Bondarenko 165143a111 Use simplexmq with client_library flag (#5133)
* Use simplexmq with client_library flag

* fix server config for mq master

* simplexmq

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-11-02 17:51:11 +00:00
Evgeny ceb17b23b4 bumped haskell.nix (#5134)
Co-authored-by: Moritz Angermann <moritz.angermann@gmail.com>
2024-11-02 15:28:41 +00:00
Evgeny Poberezkin 3c8c9d8b52 website: update jobs page 2024-11-02 13:43:45 +00:00
Stanislav Dmitrenko 4162bccc46 multiplatform: edge to edge design (#5051)
* multiplatform: insets

* more features and better performance

* calls and removed unused code

* changes

* removed logs

* status and nav bar colors

* chatList and newChatSheet search fields

* overhaul

* search fields, devtools, chatlist, newchatsheet, onehand on desktop, scrollbars

* android, desktop: update to Compose 1.7.0

- support image drag-and-drop from other applications right to a chat
(with and without transparent pixels - will be png or jpg)

* stable

* workaround

* changes

* ideal adapting height layout

* dropdownmenu, userpicker, onehandui, call layout, columns

* rename bars properties and strings

* faster update and better layout

* gallery in landscape with cutout

* better cutout

* 1% step on slider

* app bar moves to bottom in one hand ui

* default alpha

* changes

* userpicker colors

* changes

* blur

* fix wrong drawing area in chatview

* fix

* fixed differently

* changes

* changes

* android fix

* Revert "android fix"

This reverts commit 7d417afd9b.

* changes

* changes

* blur

* swap

* no logs

* fix build

* old Android support

* fix position of menu

* disable blur on Android 12

* call button padding

* useless code

* fix padding in group info view

* rename

* rename

* newline

* one more fix

* changes

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-31 17:26:17 +00:00
Stanislav Dmitrenko 24090fe350 android, desktop: update to Compose 1.7.0 (#5038)
* docs: correction

* android, desktop: update to Compose 1.7.0

- support image drag-and-drop from other applications right to a chat
(with and without transparent pixels - will be png or jpg)

* stable

* workaround

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-31 17:11:26 +00:00
spaced4ndy 37b78edb91 ios: move Network and servers settings modules to folder (#5110) 2024-10-28 18:18:26 +04:00
162 changed files with 4606 additions and 2517 deletions
+1 -18
View File
@@ -131,10 +131,6 @@ jobs:
echo " extra-include-dirs: /opt/homebrew/opt/openssl@1.1/include" >> cabal.project.local echo " extra-include-dirs: /opt/homebrew/opt/openssl@1.1/include" >> cabal.project.local
echo " extra-lib-dirs: /opt/homebrew/opt/openssl@1.1/lib" >> cabal.project.local echo " extra-lib-dirs: /opt/homebrew/opt/openssl@1.1/lib" >> cabal.project.local
echo " flags: +openssl" >> cabal.project.local echo " flags: +openssl" >> cabal.project.local
echo "" >> cabal.project.local
echo "package rocksdb-haskell-jprupp" >> cabal.project.local
echo " extra-include-dirs: /opt/homebrew/opt/rocksdb/include" >> cabal.project.local
echo " extra-lib-dirs: /opt/homebrew/opt/rocksdb/lib" >> cabal.project.local
- name: Unix prepare cabal.project.local for Mac - name: Unix prepare cabal.project.local for Mac
if: matrix.os == 'macos-13' if: matrix.os == 'macos-13'
@@ -149,22 +145,14 @@ jobs:
echo " extra-include-dirs: /usr/local/opt/openssl@1.1/include" >> cabal.project.local echo " extra-include-dirs: /usr/local/opt/openssl@1.1/include" >> cabal.project.local
echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local
echo " flags: +openssl" >> cabal.project.local echo " flags: +openssl" >> cabal.project.local
echo "" >> cabal.project.local
echo "package rocksdb-haskell-jprupp" >> cabal.project.local
echo " extra-include-dirs: /opt/homebrew/opt/rocksdb/include" >> cabal.project.local
echo " extra-lib-dirs: /opt/homebrew/opt/rocksdb/lib" >> cabal.project.local
- name: Install AppImage dependencies - name: Install AppImage dependencies
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04' if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
run: sudo apt install -y desktop-file-utils run: sudo apt install -y desktop-file-utils
- name: Install package dependencies
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04'
run: sudo apt install -y librocksdb-dev
- name: Install pkg-config for Mac - name: Install pkg-config for Mac
if: matrix.os == 'macos-latest' || matrix.os == 'macos-13' if: matrix.os == 'macos-latest' || matrix.os == 'macos-13'
run: brew install pkg-config rocksdb run: brew install pkg-config
- name: Unix prepare cabal.project.local for Ubuntu - name: Unix prepare cabal.project.local for Ubuntu
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04' if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04'
@@ -334,7 +322,6 @@ jobs:
git git
perl perl
make make
mingw-w64-x86_64-rocksdb
pacboy: >- pacboy: >-
toolchain:p toolchain:p
cmake:p cmake:p
@@ -354,10 +341,6 @@ jobs:
echo " flags: +openssl" >> cabal.project.local echo " flags: +openssl" >> cabal.project.local
echo " extra-include-dirs: $openssl_windows_style_path\include" >> cabal.project.local echo " extra-include-dirs: $openssl_windows_style_path\include" >> cabal.project.local
echo " extra-lib-dirs: $openssl_windows_style_path" >> cabal.project.local echo " extra-lib-dirs: $openssl_windows_style_path" >> cabal.project.local
echo "" >> cabal.project.local
echo "package rocksdb-haskell-jprupp" >> cabal.project.local
echo " extra-include-dirs: /mingw64/include" >> cabal.project.local
echo " extra-lib-dirs: /mingw64/lib" >> cabal.project.local
rm -rf dist-newstyle/src/direct-sq* rm -rf dist-newstyle/src/direct-sq*
sed -i "s/, unix /--, unix /" simplex-chat.cabal sed -i "s/, unix /--, unix /" simplex-chat.cabal
+1 -1
View File
@@ -29,7 +29,7 @@ RUN cp ./scripts/cabal.project.local.linux ./cabal.project.local
# Compile simplex-chat # Compile simplex-chat
RUN cabal update RUN cabal update
RUN cabal build exe:simplex-chat RUN cabal build exe:simplex-chat --constraint 'simplexmq +client_library'
# Strip the binary from debug symbols to reduce size # Strip the binary from debug symbols to reduce size
RUN bin=$(find /project/dist-newstyle -name "simplex-chat" -type f -executable) && \ RUN bin=$(find /project/dist-newstyle -name "simplex-chat" -type f -executable) && \
+21 -13
View File
@@ -149,9 +149,9 @@
6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; }; 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; };
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; }; 6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; };
643B3B452CCBEB080083A2CF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B402CCBEB080083A2CF /* libgmpxx.a */; }; 643B3B452CCBEB080083A2CF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B402CCBEB080083A2CF /* libgmpxx.a */; };
643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a */; }; 643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a */; };
643B3B472CCBEB080083A2CF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B422CCBEB080083A2CF /* libffi.a */; }; 643B3B472CCBEB080083A2CF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B422CCBEB080083A2CF /* libffi.a */; };
643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */; }; 643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a */; };
643B3B492CCBEB080083A2CF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B442CCBEB080083A2CF /* libgmp.a */; }; 643B3B492CCBEB080083A2CF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B442CCBEB080083A2CF /* libgmp.a */; };
6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; }; 6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; };
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; }; 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; };
@@ -492,9 +492,9 @@
6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = "<group>"; }; 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = "<group>"; };
6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; }; 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; };
643B3B402CCBEB080083A2CF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmpxx.a; path = Libraries/libgmpxx.a; sourceTree = "<group>"; }; 643B3B402CCBEB080083A2CF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmpxx.a; path = Libraries/libgmpxx.a; sourceTree = "<group>"; };
643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a"; sourceTree = "<group>"; }; 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a"; sourceTree = "<group>"; };
643B3B422CCBEB080083A2CF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libffi.a; path = Libraries/libffi.a; sourceTree = "<group>"; }; 643B3B422CCBEB080083A2CF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libffi.a; path = Libraries/libffi.a; sourceTree = "<group>"; };
643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a"; sourceTree = "<group>"; }; 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a"; sourceTree = "<group>"; };
643B3B442CCBEB080083A2CF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmp.a; path = Libraries/libgmp.a; sourceTree = "<group>"; }; 643B3B442CCBEB080083A2CF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmp.a; path = Libraries/libgmp.a; sourceTree = "<group>"; };
6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; }; 6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; };
6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; }; 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; };
@@ -663,8 +663,8 @@
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a in Frameworks */, 643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a in Frameworks */,
643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a in Frameworks */, 643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -815,8 +815,8 @@
643B3B422CCBEB080083A2CF /* libffi.a */, 643B3B422CCBEB080083A2CF /* libffi.a */,
643B3B442CCBEB080083A2CF /* libgmp.a */, 643B3B442CCBEB080083A2CF /* libgmp.a */,
643B3B402CCBEB080083A2CF /* libgmpxx.a */, 643B3B402CCBEB080083A2CF /* libgmpxx.a */,
643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a */, 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a */,
643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */, 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a */,
5CA059C2279559F40002BEB4 /* Shared */, 5CA059C2279559F40002BEB4 /* Shared */,
5CDCAD462818589900503DA2 /* SimpleX NSE */, 5CDCAD462818589900503DA2 /* SimpleX NSE */,
CEE723A82C3BD3D70009AE93 /* SimpleX SE */, CEE723A82C3BD3D70009AE93 /* SimpleX SE */,
@@ -912,10 +912,9 @@
5CB924DF27A8678B00ACCCDD /* UserSettings */ = { 5CB924DF27A8678B00ACCCDD /* UserSettings */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
643B3B4C2CCFD34B0083A2CF /* NetworkAndServers */,
5CB924D627A8563F00ACCCDD /* SettingsView.swift */, 5CB924D627A8563F00ACCCDD /* SettingsView.swift */,
5CB346E62868D76D001FD2EF /* NotificationsView.swift */, 5CB346E62868D76D001FD2EF /* NotificationsView.swift */,
5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */,
5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */,
5CADE79929211BB900072E13 /* PreferencesView.swift */, 5CADE79929211BB900072E13 /* PreferencesView.swift */,
5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */, 5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */,
5C05DF522840AA1D00C683F9 /* CallSettings.swift */, 5C05DF522840AA1D00C683F9 /* CallSettings.swift */,
@@ -923,9 +922,6 @@
5CC036DF29C488D500C0EF20 /* HiddenProfileView.swift */, 5CC036DF29C488D500C0EF20 /* HiddenProfileView.swift */,
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */, 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */,
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */, 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */,
5C93292E29239A170090FFF9 /* ProtocolServersView.swift */,
5C93293029239BED0090FFF9 /* ProtocolServerView.swift */,
5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */,
5CB2084E28DA4B4800D024EC /* RTCServers.swift */, 5CB2084E28DA4B4800D024EC /* RTCServers.swift */,
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */, 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */,
18415845648CA4F5A8BCA272 /* UserProfilesView.swift */, 18415845648CA4F5A8BCA272 /* UserProfilesView.swift */,
@@ -1056,6 +1052,18 @@
path = Database; path = Database;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
643B3B4C2CCFD34B0083A2CF /* NetworkAndServers */ = {
isa = PBXGroup;
children = (
5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */,
5C93293029239BED0090FFF9 /* ProtocolServerView.swift */,
5C93292E29239A170090FFF9 /* ProtocolServersView.swift */,
5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */,
5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */,
);
path = NetworkAndServers;
sourceTree = "<group>";
};
6440CA01288AEC770062C672 /* Group */ = { 6440CA01288AEC770062C672 /* Group */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -4,8 +4,10 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.* import android.os.*
import android.view.View
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.ClipboardManager
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import chat.simplex.app.model.NtfManager import chat.simplex.app.model.NtfManager
@@ -13,7 +15,6 @@ import chat.simplex.app.model.NtfManager.getUserIdFromIntent
import chat.simplex.common.* import chat.simplex.common.*
import chat.simplex.common.helpers.* import chat.simplex.common.helpers.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
@@ -24,13 +25,21 @@ import kotlinx.coroutines.*
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
class MainActivity: FragmentActivity() { class MainActivity: FragmentActivity() {
companion object {
const val OLD_ANDROID_UI_FLAGS = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
mainActivity = WeakReference(this) mainActivity = WeakReference(this)
platform.androidSetNightModeIfSupported() platform.androidSetNightModeIfSupported()
val c = CurrentColors.value.colors val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight)
applyAppLocale(ChatModel.controller.appPrefs.appLanguage) applyAppLocale(ChatModel.controller.appPrefs.appLanguage)
// This flag makes status bar and navigation bar fully transparent. But on API level < 30 it breaks insets entirely
// https://issuetracker.google.com/issues/236862874
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
}
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// testJson() // testJson()
// When call ended and orientation changes, it re-process old intent, it's unneeded. // When call ended and orientation changes, it re-process old intent, it's unneeded.
@@ -47,6 +56,7 @@ class MainActivity: FragmentActivity() {
WindowManager.LayoutParams.FLAG_SECURE WindowManager.LayoutParams.FLAG_SECURE
) )
} }
enableEdgeToEdge()
setContent { setContent {
AppScreen() AppScreen()
} }
@@ -7,6 +7,7 @@ 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.os.* import android.os.*
import android.view.View
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@@ -16,6 +17,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.lifecycle.* import androidx.lifecycle.*
import androidx.work.* import androidx.work.*
import chat.simplex.app.MainActivity.Companion.OLD_ANDROID_UI_FLAGS
import chat.simplex.app.model.NtfManager import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.AcceptCallAction import chat.simplex.app.model.NtfManager.AcceptCallAction
import chat.simplex.app.views.call.CallActivity import chat.simplex.app.views.call.CallActivity
@@ -26,7 +28,6 @@ import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.* import chat.simplex.common.views.call.*
import chat.simplex.common.views.chatlist.statusBarColorAfterCall
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.onboarding.OnboardingStage
import com.jakewharton.processphoenix.ProcessPhoenix import com.jakewharton.processphoenix.ProcessPhoenix
@@ -274,79 +275,32 @@ class SimplexApp: Application(), LifecycleEventObserver {
uiModeManager.setApplicationNightMode(mode) uiModeManager.setApplicationNightMode(mode)
} }
override fun androidSetDrawerStatusAndNavBarColor( override fun androidSetStatusAndNavigationBarAppearance(isLightStatusBar: Boolean, isLightNavBar: Boolean, blackNavBar: Boolean, themeBackgroundColor: Color) {
isLight: Boolean,
drawerShadingColor: Color,
toolbarOnTop: Boolean,
navBarColor: Color,
) {
val window = mainActivity.get()?.window ?: return
@Suppress("DEPRECATION")
val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView)
// Blend status bar color to the animated color
val colors = CurrentColors.value.colors
val baseBackgroundColor = if (toolbarOnTop) colors.background.mixWith(colors.onBackground, 0.97f) else colors.background
var statusBar = baseBackgroundColor.mixWith(drawerShadingColor.copy(1f), 1 - drawerShadingColor.alpha).toArgb()
var statusBarLight = isLight
// SimplexGreen while in call
if (window.statusBarColor == SimplexGreen.toArgb()) {
statusBarColorAfterCall.intValue = statusBar
statusBar = SimplexGreen.toArgb()
statusBarLight = false
}
window.statusBarColor = statusBar
val navBar = navBarColor.toArgb()
if (windowInsetController?.isAppearanceLightStatusBars != statusBarLight) {
windowInsetController?.isAppearanceLightStatusBars = statusBarLight
}
if (window.navigationBarColor != navBar) {
window.navigationBarColor = navBar
}
if (windowInsetController?.isAppearanceLightNavigationBars != isLight) {
windowInsetController?.isAppearanceLightNavigationBars = isLight
}
}
override fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {
val window = mainActivity.get()?.window ?: return val window = mainActivity.get()?.window ?: return
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
val statusLight = isLightStatusBar && chatModel.activeCall.value == null
val navBarLight = isLightNavBar || windowOrientation() == WindowOrientation.LANDSCAPE
val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView) val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView)
if (windowInsetController?.isAppearanceLightStatusBars != statusLight) {
var statusBar = (if (hasTop && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) { windowInsetController?.isAppearanceLightStatusBars = statusLight
backgroundColor.mixWith(CurrentColors.value.colors.onBackground, 0.97f) }
window.navigationBarColor = Color.Transparent.toArgb()
if (windowInsetController?.isAppearanceLightNavigationBars != navBarLight) {
windowInsetController?.isAppearanceLightNavigationBars = navBarLight
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
window.decorView.systemUiVisibility = if (statusLight && navBarLight) {
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or OLD_ANDROID_UI_FLAGS
} else if (statusLight) {
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or OLD_ANDROID_UI_FLAGS
} else if (navBarLight) {
View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or OLD_ANDROID_UI_FLAGS
} else { } else {
if (CurrentColors.value.base == DefaultTheme.SIMPLEX) { OLD_ANDROID_UI_FLAGS
backgroundColor.lighter(0.4f) }
window.navigationBarColor = if (blackNavBar) Color.Black.toArgb() else themeBackgroundColor.toArgb()
} else { } else {
backgroundColor window.navigationBarColor = Color.Transparent.toArgb()
}
}).toArgb()
var statusBarLight = isLight
// SimplexGreen while in call
if (window.statusBarColor == SimplexGreen.toArgb()) {
statusBarColorAfterCall.intValue = statusBar
statusBar = SimplexGreen.toArgb()
statusBarLight = false
}
val navBar = (if (hasBottom && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
backgroundColor.mixWith(CurrentColors.value.colors.onBackground, 0.97f)
} else {
backgroundColor
}).toArgb()
if (window.statusBarColor != statusBar) {
window.statusBarColor = statusBar
}
if (windowInsetController?.isAppearanceLightStatusBars != statusBarLight) {
windowInsetController?.isAppearanceLightStatusBars = statusBarLight
}
if (window.navigationBarColor != navBar) {
window.navigationBarColor = navBar
}
if (windowInsetController?.isAppearanceLightNavigationBars != isLight) {
windowInsetController?.isAppearanceLightNavigationBars = isLight
} }
} }
@@ -401,6 +355,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
} }
return true return true
} }
override val androidApiLevel: Int get() = Build.VERSION.SDK_INT
} }
} }
} }
@@ -64,7 +64,6 @@ kotlin {
implementation("androidx.activity:activity-compose:1.9.1") implementation("androidx.activity:activity-compose:1.9.1")
val workVersion = "2.9.1" val workVersion = "2.9.1"
implementation("androidx.work:work-runtime-ktx:$workVersion") implementation("androidx.work:work-runtime-ktx:$workVersion")
implementation("com.google.accompanist:accompanist-insets:0.30.1")
// Video support // Video support
implementation("com.google.android.exoplayer:exoplayer:2.19.1") implementation("com.google.android.exoplayer:exoplayer:2.19.1")
@@ -3,25 +3,13 @@ package chat.simplex.common.platform
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import com.google.accompanist.insets.navigationBarsWithImePadding
import java.io.File import java.io.File
actual fun Modifier.navigationBarsWithImePadding(): Modifier = navigationBarsWithImePadding()
@Composable
actual fun ProvideWindowInsets(
consumeWindowInsets: Boolean,
windowInsetsAnimationsEnabled: Boolean,
content: @Composable () -> Unit
) {
com.google.accompanist.insets.ProvideWindowInsets(content = content)
}
@Composable @Composable
actual fun Modifier.desktopOnExternalDrag( actual fun Modifier.desktopOnExternalDrag(
enabled: Boolean, enabled: Boolean,
onFiles: (List<File>) -> Unit, onFiles: (List<File>) -> Unit,
onImage: (Painter) -> Unit, onImage: (File) -> Unit,
onText: (String) -> Unit onText: (String) -> Unit
): Modifier = this ): Modifier = this
@@ -6,8 +6,7 @@ import android.graphics.drawable.ColorDrawable
import android.os.Build import android.os.Build
import android.text.InputType import android.text.InputType
import android.util.Log import android.util.Log
import android.view.OnReceiveContentListener import android.view.*
import android.view.ViewGroup
import android.view.inputmethod.* import android.view.inputmethod.*
import android.widget.EditText import android.widget.EditText
import android.widget.TextView import android.widget.TextView
@@ -26,6 +25,7 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.children
import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
@@ -94,8 +94,8 @@ actual fun PlatformTextField(
} }
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
AndroidView(modifier = Modifier, factory = { AndroidView(modifier = Modifier, factory = { context ->
val editText = @SuppressLint("AppCompatCustomView") object: EditText(it) { val editText = @SuppressLint("AppCompatCustomView") object: EditText(context) {
override fun setOnReceiveContentListener( override fun setOnReceiveContentListener(
mimeTypes: Array<out String>?, mimeTypes: Array<out String>?,
listener: OnReceiveContentListener? listener: OnReceiveContentListener?
@@ -140,6 +140,13 @@ actual fun PlatformTextField(
Log.e(TAG, e.stackTraceToString()) Log.e(TAG, e.stackTraceToString())
} }
} }
editText.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
// shows keyboard when user had search field on ChatView focused before clicking on this text field
// it still produce weird animation of closing/opening keyboard but the solution is to replace this Android EditText with Compose BasicTextField
if (hasFocus) {
showKeyboard = true
}
}
editText.doOnTextChanged { text, _, _, _ -> editText.doOnTextChanged { text, _, _, _ ->
if (!composeState.value.inProgress) { if (!composeState.value.inProgress) {
onMessageChange(text.toString()) onMessageChange(text.toString())
@@ -148,8 +155,12 @@ actual fun PlatformTextField(
} }
} }
editText.doAfterTextChanged { text -> if (composeState.value.preview is ComposePreview.VoicePreview && text.toString() != "") editText.setText("") } editText.doAfterTextChanged { text -> if (composeState.value.preview is ComposePreview.VoicePreview && text.toString() != "") editText.setText("") }
editText val workaround = WorkaroundFocusSearchLayout(context)
workaround.addView(editText)
workaround.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
workaround
}) { }) {
val it = it.children.first() as EditText
it.setTextColor(textColor.toArgb()) it.setTextColor(textColor.toArgb())
it.setHintTextColor(hintColor.toArgb()) it.setHintTextColor(hintColor.toArgb())
it.hint = placeholder it.hint = placeholder
@@ -6,11 +6,11 @@ import android.content.Context
import android.content.SharedPreferences 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.foundation.layout.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.*
import androidx.compose.ui.platform.LocalContext
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
@@ -50,7 +50,11 @@ actual fun windowOrientation(): WindowOrientation = when (mainActivity.get()?.re
} }
@Composable @Composable
actual fun windowWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp actual fun windowWidth(): Dp {
val direction = LocalLayoutDirection.current
val cutout = WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal).asPaddingValues()
return LocalConfiguration.current.screenWidthDp.dp - cutout.calculateStartPadding(direction) - cutout.calculateEndPadding(direction)
}
@Composable @Composable
actual fun windowHeight(): Dp = LocalConfiguration.current.screenHeightDp.dp actual fun windowHeight(): Dp = LocalConfiguration.current.screenHeightDp.dp
@@ -7,10 +7,12 @@ import androidx.compose.foundation.lazy.*
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
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.views.chatlist.NavigationBarBackground
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@@ -25,25 +27,74 @@ actual fun LazyColumnWithScrollBar(
horizontalAlignment: Alignment.Horizontal, horizontalAlignment: Alignment.Horizontal,
flingBehavior: FlingBehavior, flingBehavior: FlingBehavior,
userScrollEnabled: Boolean, userScrollEnabled: Boolean,
additionalBarOffset: State<Dp>?,
fillMaxSize: Boolean,
content: LazyListScope.() -> Unit content: LazyListScope.() -> Unit
) { ) {
val state = state ?: LocalAppBarHandler.current?.listState ?: rememberLazyListState() val handler = LocalAppBarHandler.current
val connection = LocalAppBarHandler.current?.connection require(handler != null) { "Using LazyColumnWithScrollBar and without AppBarHandler is an error. Use LazyColumnWithScrollBarNoAppBar instead" }
val state = state ?: handler.listState
val connection = handler.connection
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (reverseLayout) {
snapshotFlow { state.layoutInfo.visibleItemsInfo.lastOrNull()?.offset ?: 0 }
.collect { scrollPosition ->
connection.appBarOffset = if (state.layoutInfo.visibleItemsInfo.lastOrNull()?.index == state.layoutInfo.totalItemsCount - 1) {
state.layoutInfo.viewportEndOffset - scrollPosition.toFloat() - state.layoutInfo.afterContentPadding
} else {
// show always when last item is not visible
-1000f
}
//Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
}
} else {
snapshotFlow { state.firstVisibleItemScrollOffset } snapshotFlow { state.firstVisibleItemScrollOffset }
.filter { state.firstVisibleItemIndex == 0 } .filter { state.firstVisibleItemIndex == 0 }
.collect { scrollPosition -> .collect { scrollPosition ->
val offset = connection?.appBarOffset val offset = connection.appBarOffset
if (offset != null && (offset + scrollPosition).absoluteValue > 1) { if ((offset + scrollPosition + state.layoutInfo.afterContentPadding).absoluteValue > 1) {
connection.appBarOffset = -scrollPosition.toFloat() connection.appBarOffset = -scrollPosition.toFloat()
//Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") //Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
} }
} }
} }
if (connection != null) { }
LazyColumn(modifier.nestedScroll(connection), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) LazyColumn(
if (fillMaxSize) {
Modifier.fillMaxSize().copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection)
} else { } else {
LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection)
},
state,
contentPadding,
reverseLayout,
verticalArrangement,
horizontalAlignment,
flingBehavior,
userScrollEnabled
) {
content()
}
}
@Composable
actual fun LazyColumnWithScrollBarNoAppBar(
modifier: Modifier,
state: LazyListState?,
contentPadding: PaddingValues,
reverseLayout: Boolean,
verticalArrangement: Arrangement.Vertical,
horizontalAlignment: Alignment.Horizontal,
flingBehavior: FlingBehavior,
userScrollEnabled: Boolean,
additionalBarOffset: State<Dp>?,
content: LazyListScope.() -> Unit
) {
val state = state ?: rememberLazyListState()
LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled) {
content()
} }
} }
@@ -54,32 +105,80 @@ actual fun ColumnWithScrollBar(
horizontalAlignment: Alignment.Horizontal, horizontalAlignment: Alignment.Horizontal,
state: ScrollState?, state: ScrollState?,
maxIntrinsicSize: Boolean, maxIntrinsicSize: Boolean,
fillMaxSize: Boolean,
content: @Composable() (ColumnScope.() -> Unit) content: @Composable() (ColumnScope.() -> Unit)
) { ) {
val state = state ?: LocalAppBarHandler.current?.scrollState ?: rememberScrollState() val handler = LocalAppBarHandler.current
val connection = LocalAppBarHandler.current?.connection require(handler != null) { "Using ColumnWithScrollBar and without AppBarHandler is an error. Use ColumnWithScrollBarNoAppBar instead" }
val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier).imePadding() else modifier.imePadding()
val state = state ?: handler.scrollState
val connection = handler.connection
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
snapshotFlow { state.value } snapshotFlow { state.value }
.collect { scrollPosition -> .collect { scrollPosition ->
val offset = connection?.appBarOffset val offset = connection.appBarOffset
if (offset != null && (offset + scrollPosition).absoluteValue > 1) { if ((offset + scrollPosition).absoluteValue > 1) {
connection.appBarOffset = -scrollPosition.toFloat() connection.appBarOffset = -scrollPosition.toFloat()
// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") // Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
} }
} }
} }
if (connection != null) { val oneHandUI = remember { appPrefs.oneHandUI.state }
Box(Modifier.fillMaxHeight()) {
Column( Column(
if (maxIntrinsicSize) { if (maxIntrinsicSize) {
modifier.nestedScroll(connection).verticalScroll(state).height(IntrinsicSize.Max) Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection).verticalScroll(state).height(IntrinsicSize.Max)
} else { } else {
modifier.nestedScroll(connection).verticalScroll(state) Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection).verticalScroll(state)
}, verticalArrangement, horizontalAlignment, content) }, verticalArrangement, horizontalAlignment
) {
if (oneHandUI.value) {
Spacer(Modifier.padding(top = DEFAULT_PADDING + 5.dp).windowInsetsTopHeight(WindowInsets.statusBars))
content()
Spacer(Modifier.navigationBarsPadding().padding(bottom = AppBarHeight * fontSizeSqrtMultiplier))
} else { } else {
Column(if (maxIntrinsicSize) { Spacer(Modifier.statusBarsPadding().padding(top = AppBarHeight * fontSizeSqrtMultiplier))
content()
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
}
}
if (!oneHandUI.value) {
NavigationBarBackground(false, false)
}
}
}
@Composable
actual fun ColumnWithScrollBarNoAppBar(
modifier: Modifier,
verticalArrangement: Arrangement.Vertical,
horizontalAlignment: Alignment.Horizontal,
state: ScrollState?,
maxIntrinsicSize: Boolean,
content: @Composable() (ColumnScope.() -> Unit)
) {
val modifier = modifier.imePadding()
val state = state ?: rememberScrollState()
val oneHandUI = remember { appPrefs.oneHandUI.state }
Box(Modifier.fillMaxHeight()) {
Column(
if (maxIntrinsicSize) {
modifier.verticalScroll(state).height(IntrinsicSize.Max) modifier.verticalScroll(state).height(IntrinsicSize.Max)
} else { } else {
modifier.verticalScroll(state) modifier.verticalScroll(state)
}, verticalArrangement, horizontalAlignment, content) }, verticalArrangement, horizontalAlignment
) {
if (oneHandUI.value) {
Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars))
content()
} else {
content()
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
}
}
if (!oneHandUI.value) {
NavigationBarBackground(false, false)
}
} }
} }
@@ -3,17 +3,18 @@ package chat.simplex.common.platform
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.graphics.Rect
import android.os.* import android.os.*
import android.view.* import android.view.*
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.ime
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import chat.simplex.common.AppScreen import chat.simplex.common.AppScreen
import chat.simplex.common.model.clear import chat.simplex.common.model.clear
import chat.simplex.common.ui.theme.SimpleXTheme
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import androidx.compose.ui.platform.LocalContext as LocalContext1 import androidx.compose.ui.platform.LocalContext as LocalContext1
import chat.simplex.res.MR import chat.simplex.res.MR
@@ -43,28 +44,13 @@ actual fun LocalMultiplatformView(): Any? = LocalView.current
@Composable @Composable
actual fun getKeyboardState(): State<KeyboardState> { actual fun getKeyboardState(): State<KeyboardState> {
val keyboardState = remember { mutableStateOf(KeyboardState.Closed) } val density = LocalDensity.current
val view = LocalView.current val ime = WindowInsets.ime
DisposableEffect(view) { return remember {
val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener { derivedStateOf {
val rect = Rect() if (ime.getBottom(density) == 0) KeyboardState.Closed else KeyboardState.Opened
view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height
val keypadHeight = screenHeight - rect.bottom
keyboardState.value = if (keypadHeight > screenHeight * 0.15) {
KeyboardState.Opened
} else {
KeyboardState.Closed
} }
} }
view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)
onDispose {
view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
}
}
return keyboardState
} }
actual fun hideKeyboard(view: Any?, clearFocus: Boolean) { actual fun hideKeyboard(view: Any?, clearFocus: Boolean) {
@@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape 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.material.ripple.rememberRipple
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
@@ -48,6 +47,7 @@ import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.res.MR import chat.simplex.res.MR
import com.google.accompanist.permissions.* import com.google.accompanist.permissions.*
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
@@ -329,11 +329,14 @@ private fun ActiveCallOverlayLayout(
flipCamera: () -> Unit flipCamera: () -> Unit
) { ) {
Column { Column {
CloseSheetBar({ chatModel.activeCallViewIsCollapsed.value = true }, true, tintColor = Color(0xFFFFFFD8)) { CallAppBar(
title = {
if (call.hasVideo) { if (call.hasVideo) {
Text(call.contact.chatViewName, Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1) Text(call.contact.chatViewName, Modifier.offset(x = (-4).dp).padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1)
}
} }
},
onBack = { chatModel.activeCallViewIsCollapsed.value = true }
)
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
@Composable @Composable
fun SelectSoundDevice(size: Dp) { fun SelectSoundDevice(size: Dp) {
@@ -411,6 +414,7 @@ private fun ControlButton(icon: Painter, iconText: StringResource, enabled: Bool
@Composable @Composable
private fun ControlButtonWrap(enabled: Boolean = true, action: () -> Unit, background: Color = controlButtonsBackground(), size: Dp, content: @Composable () -> Unit) { private fun ControlButtonWrap(enabled: Boolean = true, action: () -> Unit, background: Color = controlButtonsBackground(), size: Dp, content: @Composable () -> Unit) {
val ripple = remember { ripple(bounded = false, radius = size / 2, color = background.lighter(0.1f)) }
Box( Box(
Modifier Modifier
.background(background, CircleShape) .background(background, CircleShape)
@@ -419,7 +423,7 @@ private fun ControlButtonWrap(enabled: Boolean = true, action: () -> Unit, backg
onClick = action, onClick = action,
role = Role.Button, role = Role.Button,
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = size / 2, color = background.lighter(0.1f)), indication = ripple,
enabled = enabled enabled = enabled
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -590,8 +594,9 @@ fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Uni
} }
} }
} else { } else {
ModalView(background = Color.Black, showClose = false, close = {}) { ModalView(background = Color.Black, showAppBar = false, close = {}) {
ColumnWithScrollBar(Modifier.fillMaxSize()) { Column {
Spacer(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier))
AppBarTitle(stringResource(MR.strings.permissions_required)) AppBarTitle(stringResource(MR.strings.permissions_required))
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
val onClick = { val onClick = {
@@ -8,7 +8,6 @@ import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import chat.simplex.common.model.CIFile import chat.simplex.common.model.CIFile
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.common.views.helpers.ModalManager import chat.simplex.common.views.helpers.ModalManager
@@ -39,14 +38,6 @@ actual fun SimpleAndAnimatedImageView(
if (getLoadedFilePath(file) != null) { if (getLoadedFilePath(file) != null) {
ModalManager.fullscreen.showCustomModal(animated = false) { close -> ModalManager.fullscreen.showCustomModal(animated = false) { close ->
ImageFullScreenView(imageProvider, close) ImageFullScreenView(imageProvider, close)
if (smallView) {
DisposableEffect(Unit) {
onDispose {
val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
}
}
}
} }
} }
} }
@@ -1,32 +1,26 @@
package chat.simplex.common.views.chatlist package chat.simplex.common.views.chatlist
import android.app.Activity
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.ripple.rememberRipple
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
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.*
import androidx.compose.ui.unit.sp
import chat.simplex.common.ANDROID_CALL_TOP_PADDING import chat.simplex.common.ANDROID_CALL_TOP_PADDING
import chat.simplex.common.model.durationText import chat.simplex.common.model.durationText
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.* import chat.simplex.common.views.call.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
private val CALL_INTERACTIVE_AREA_HEIGHT = 74.dp private val CALL_INTERACTIVE_AREA_HEIGHT = 74.dp
@@ -38,11 +32,12 @@ private val CALL_BOTTOM_ICON_HEIGHT = CALL_INTERACTIVE_AREA_HEIGHT + CALL_BOTTOM
@Composable @Composable
actual fun ActiveCallInteractiveArea(call: Call) { actual fun ActiveCallInteractiveArea(call: Call) {
val onClick = { platform.androidStartCallActivity(false) } val onClick = { platform.androidStartCallActivity(false) }
Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT)) { val statusBar = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT + statusBar)) {
val source = remember { MutableInteractionSource() } val source = remember { MutableInteractionSource() }
val indication = rememberRipple(bounded = true, 3000.dp) val ripple = remember { ripple(bounded = true, 3000.dp) }
Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT).clickable(onClick = onClick, indication = indication, interactionSource = source)) { Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT + statusBar).clickable(onClick = onClick, indication = ripple, interactionSource = source)) {
GreenLine(call) GreenLine(statusBar, call)
} }
Box( Box(
Modifier Modifier
@@ -50,7 +45,7 @@ actual fun ActiveCallInteractiveArea(call: Call) {
.size(CALL_BOTTOM_ICON_HEIGHT) .size(CALL_BOTTOM_ICON_HEIGHT)
.background(SimplexGreen, CircleShape) .background(SimplexGreen, CircleShape)
.clip(CircleShape) .clip(CircleShape)
.clickable(onClick = onClick, indication = indication, interactionSource = source) .clickable(onClick = onClick, indication = ripple, interactionSource = source)
.align(Alignment.BottomCenter), .align(Alignment.BottomCenter),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
@@ -63,16 +58,13 @@ actual fun ActiveCallInteractiveArea(call: Call) {
} }
} }
// Temporary solution for storing a color that needs to be applied after call ends
var statusBarColorAfterCall = mutableIntStateOf(CurrentColors.value.colors.background.toArgb())
@Composable @Composable
private fun GreenLine(call: Call) { private fun GreenLine(statusBarHeight: Dp, call: Call) {
Row( Row(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.background(SimplexGreen) .background(SimplexGreen)
.padding(top = -CALL_TOP_OFFSET) .padding(top = -CALL_TOP_OFFSET + statusBarHeight)
.padding(horizontal = DEFAULT_PADDING), .padding(horizontal = DEFAULT_PADDING),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
@@ -81,12 +73,10 @@ private fun GreenLine(call: Call) {
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
CallDuration(call) CallDuration(call)
} }
val window = (LocalContext.current as Activity).window
DisposableEffect(Unit) { DisposableEffect(Unit) {
statusBarColorAfterCall.intValue = window.statusBarColor platform.androidSetStatusAndNavigationBarAppearance(false, CurrentColors.value.colors.isLight)
window.statusBarColor = SimplexGreen.toArgb()
onDispose { onDispose {
window.statusBarColor = statusBarColorAfterCall.intValue platform.androidSetStatusAndNavigationBarAppearance(CurrentColors.value.colors.isLight, CurrentColors.value.colors.isLight)
} }
} }
} }
@@ -19,13 +19,11 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.User import chat.simplex.common.model.User
import chat.simplex.common.model.UserInfo import chat.simplex.common.model.UserInfo
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -36,6 +34,7 @@ private val USER_PICKER_ROW_PADDING = 16.dp
@Composable @Composable
actual fun UserPickerUsersSection( actual fun UserPickerUsersSection(
users: List<UserInfo>, users: List<UserInfo>,
iconColor: Color,
stopped: Boolean, stopped: Boolean,
onUserClicked: (user: User) -> Unit, onUserClicked: (user: User) -> Unit,
) { ) {
@@ -140,41 +139,15 @@ actual fun PlatformUserPicker(modifier: Modifier, pickerState: MutableStateFlow<
} else { } else {
Modifier Modifier
} }
Box {
Box( Box(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.then(clickableModifier) .then(clickableModifier)
.drawBehind { .drawBehind {
val pos = when { val pos = calculatePosition(dismissState)
dismissState.progress.from == DismissValue.Default && dismissState.progress.to == DismissValue.Default -> 1f
dismissState.progress.from == DismissValue.DismissedToEnd && dismissState.progress.to == DismissValue.DismissedToEnd -> 0f
dismissState.progress.to == DismissValue.Default -> dismissState.progress.fraction
else -> 1 - dismissState.progress.fraction
}
val colors = CurrentColors.value.colors val colors = CurrentColors.value.colors
val resultingColor = if (colors.isLight) colors.onSurface.copy(alpha = ScrimOpacity) else Color.Black.copy(0.64f) val resultingColor = if (colors.isLight) colors.onSurface.copy(alpha = ScrimOpacity) else Color.Black.copy(0.64f)
val adjustedAlpha = resultingColor.alpha * calculateFraction(pos = pos)
val shadingColor = resultingColor.copy(alpha = adjustedAlpha)
if (pickerState.value.isVisible()) {
platform.androidSetDrawerStatusAndNavBarColor(
isLight = colors.isLight,
drawerShadingColor = shadingColor,
toolbarOnTop = !appPrefs.oneHandUI.get(),
navBarColor = colors.background.mixWith(colors.onBackground, 1 - userPickerAlpha())
)
} else if (ModalManager.start.modalCount.value == 0) {
platform.androidSetDrawerStatusAndNavBarColor(
isLight = colors.isLight,
drawerShadingColor = shadingColor,
toolbarOnTop = !appPrefs.oneHandUI.get(),
navBarColor = (if (appPrefs.oneHandUI.get() && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
colors.background.mixWith(CurrentColors.value.colors.onBackground, 0.97f)
} else {
colors.background
})
)
}
drawRect( drawRect(
if (pos != 0f) resultingColor else Color.Transparent, if (pos != 0f) resultingColor else Color.Transparent,
alpha = calculateFraction(pos = pos) alpha = calculateFraction(pos = pos)
@@ -214,11 +187,23 @@ actual fun PlatformUserPicker(modifier: Modifier, pickerState: MutableStateFlow<
swipeDistance = height.intValue.toFloat(), swipeDistance = height.intValue.toFloat(),
) )
else Modifier else Modifier
Box(draggableModifier.then(modifier)) { Box(draggableModifier.then(modifier).navigationBarsPadding()) {
content() content()
} }
} }
} }
NavigationBarBackground(
modifier = Modifier.graphicsLayer { alpha = if (calculatePosition(dismissState) > 0.1f) 1f else 0f },
color = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha())
)
}
}
private fun calculatePosition(dismissState: DismissState): Float = when {
dismissState.progress.from == DismissValue.Default && dismissState.progress.to == DismissValue.Default -> 1f
dismissState.progress.from == DismissValue.DismissedToEnd && dismissState.progress.to == DismissValue.DismissedToEnd -> 0f
dismissState.progress.to == DismissValue.Default -> dismissState.progress.fraction
else -> 1 - dismissState.progress.fraction
} }
private fun Modifier.draggableBottomDrawerModifier( private fun Modifier.draggableBottomDrawerModifier(
@@ -171,6 +171,8 @@ actual fun GetImageBottomSheet(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
.imePadding()
.navigationBarsPadding()
.onFocusChanged { focusState -> .onFocusChanged { focusState ->
if (!focusState.hasFocus) hideBottomSheet() if (!focusState.hasFocus) hideBottomSheet()
} }
@@ -0,0 +1,41 @@
package chat.simplex.common.views.helpers
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
/**
* A workaround for the ANR issue on Compose 1.7.x.
* https://issuetracker.google.com/issues/369354336
* Code from:
* https://issuetracker.google.com/issues/369354336#comment8
*/
class WorkaroundFocusSearchLayout : FrameLayout {
constructor(
context: Context,
) : super(context)
constructor(
context: Context,
attrs: AttributeSet?,
) : super(context, attrs)
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int,
) : super(context, attrs, defStyleAttr)
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int,
) : super(context, attrs, defStyleAttr, defStyleRes)
override fun focusSearch(focused: View?, direction: Int): View? {
return null
}
}
@@ -2,6 +2,7 @@ package chat.simplex.common.views.usersettings
import SectionBottomSpacer import SectionBottomSpacer
import SectionDividerSpaced import SectionDividerSpaced
import SectionSpacer
import SectionView import SectionView
import android.app.Activity import android.app.Activity
import android.content.ComponentName import android.content.ComponentName
@@ -31,6 +32,7 @@ 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.model.ChatController.appPrefs
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
@@ -75,9 +77,7 @@ fun AppearanceScope.AppearanceLayout(
systemDarkTheme: SharedPreference<String?>, systemDarkTheme: SharedPreference<String?>,
changeIcon: (AppIcon) -> Unit, changeIcon: (AppIcon) -> Unit,
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.appearance_settings)) AppBarTitle(stringResource(MR.strings.appearance_settings))
SectionView(stringResource(MR.strings.settings_section_title_interface), contentPadding = PaddingValues()) { SectionView(stringResource(MR.strings.settings_section_title_interface), contentPadding = PaddingValues()) {
val context = LocalContext.current val context = LocalContext.current
@@ -106,15 +106,15 @@ fun AppearanceScope.AppearanceLayout(
} }
// } // }
SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) { SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI)
val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false)
}
} }
SectionDividerSpaced() SectionDividerSpaced()
ThemesSection(systemDarkTheme) ThemesSection(systemDarkTheme)
SectionDividerSpaced()
AppToolbarsSection()
SectionDividerSpaced() SectionDividerSpaced()
MessageShapeSection() MessageShapeSection()
@@ -13,14 +13,13 @@ import dev.icerock.moko.resources.compose.stringResource
@Composable @Composable
actual fun SettingsSectionApp( actual fun SettingsSectionApp(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showVersion: () -> Unit, showVersion: () -> Unit,
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
) { ) {
SectionView(stringResource(MR.strings.settings_section_title_app)) { SectionView(stringResource(MR.strings.settings_section_title_app)) {
SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp) SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp)
SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) }) SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) })
SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }) SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(withAuth) })
AppVersionItem(showVersion) AppVersionItem(showVersion)
} }
} }
@@ -11,10 +11,13 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView
import chat.simplex.common.model.* import chat.simplex.common.model.*
@@ -39,18 +42,43 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlin.math.absoluteValue
@Composable @Composable
fun AppScreen() { fun AppScreen() {
AppBarHandler.appBarMaxHeightPx = with(LocalDensity.current) { AppBarHeight.roundToPx() } AppBarHandler.appBarMaxHeightPx = with(LocalDensity.current) { AppBarHeight.roundToPx() }
SimpleXTheme { SimpleXTheme {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) { Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
// This padding applies to landscape view only taking care of navigation bar and holes in screen in status bar area
// (because nav bar and holes located on vertical sides of screen in landscape view)
val direction = LocalLayoutDirection.current
val safePadding = WindowInsets.safeDrawing.asPaddingValues()
val cutout = WindowInsets.displayCutout.asPaddingValues()
val cutoutStart = cutout.calculateStartPadding(direction)
val cutoutEnd = cutout.calculateEndPadding(direction)
val cutoutMax = maxOf(cutoutStart, cutoutEnd)
val paddingStartUntouched = safePadding.calculateStartPadding(direction)
val paddingStart = paddingStartUntouched - cutoutStart
val paddingEndUntouched = safePadding.calculateEndPadding(direction)
val paddingEnd = paddingEndUntouched - cutoutEnd
// Such a strange layout is needed because the main content should be covered by solid color in order to hide overflow
// of some elements that may have negative offset (so, can't use Row {}).
// To check: go to developer settings of Android, choose Display cutout -> Punch hole, and rotate the phone to landscape, open any chat
Box {
val fullscreenGallery = remember { chatModel.fullscreenGalleryVisible }
Box(Modifier.padding(start = paddingStart + cutoutMax, end = paddingEnd + cutoutMax).consumeWindowInsets(PaddingValues(start = paddingStartUntouched, end = paddingEndUntouched))) {
Box(Modifier.drawBehind {
if (fullscreenGallery.value) {
drawRect(Color.Black, topLeft = Offset(-(paddingStart + cutoutMax).toPx(), 0f), Size(size.width + (paddingStart + cutoutMax).toPx() + (paddingEnd + cutoutMax).toPx(), size.height))
}
}) {
MainScreen() MainScreen()
} }
} }
} }
} }
}
}
@Composable @Composable
fun MainScreen() { fun MainScreen() {
@@ -138,7 +166,9 @@ fun MainScreen() {
} }
SetupClipboardListener() SetupClipboardListener()
if (appPlatform.isAndroid) { if (appPlatform.isAndroid) {
AndroidWrapInCallLayout {
AndroidScreen(userPickerState) AndroidScreen(userPickerState)
}
} else { } else {
DesktopScreen(userPickerState) DesktopScreen(userPickerState)
} }
@@ -170,7 +200,9 @@ fun MainScreen() {
} }
} }
if (appPlatform.isAndroid) { if (appPlatform.isAndroid) {
AndroidWrapInCallLayout {
ModalManager.fullscreen.showInView() ModalManager.fullscreen.showInView()
}
SwitchingUsersView() SwitchingUsersView()
} }
@@ -238,18 +270,38 @@ fun MainScreen() {
val ANDROID_CALL_TOP_PADDING = 40.dp val ANDROID_CALL_TOP_PADDING = 40.dp
@Composable @Composable
fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) { fun AndroidWrapInCallLayout(content: @Composable () -> Unit) {
BoxWithConstraints {
val call = remember { chatModel.activeCall}.value val call = remember { chatModel.activeCall}.value
val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted
Box {
Box(Modifier.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)) {
content()
}
if (call != null && showCallArea) {
ActiveCallInteractiveArea(call)
}
}
}
@Composable
fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
BoxWithConstraints {
val currentChatId = remember { mutableStateOf(chatModel.chatId.value) } val currentChatId = remember { mutableStateOf(chatModel.chatId.value) }
val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) } val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) }
val cutout = WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal).asPaddingValues()
val direction = LocalLayoutDirection.current
val hasCutout = cutout.calculateStartPadding(direction) + cutout.calculateEndPadding(direction) > 0.dp
Box( Box(
Modifier Modifier
// clipping only for devices with cutout currently visible on sides. It prevents showing chat list with open chat view
// In order cases it's not needed to use clip
.then(if (hasCutout) Modifier.clip(RectangleShape) else Modifier)
.graphicsLayer { .graphicsLayer {
translationX = -offset.value.dp.toPx() // minOf thing is needed for devices with holes in screen while the user on ChatView rotates his phone from portrait to landscape
// because in this case (at least in emulator) maxWidth changes in two steps: big first, smaller on next frame.
// But offset is remembered already, so this is a better way than dropping a value of offset
translationX = -minOf(offset.value.dp, maxWidth).toPx()
} }
.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)
) { ) {
StartPartOfScreen(userPickerState) StartPartOfScreen(userPickerState)
} }
@@ -271,53 +323,42 @@ fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
snapshotFlow { chatModel.chatId.value } snapshotFlow { chatModel.chatId.value }
.distinctUntilChanged() .distinctUntilChanged()
.collect { .collect {
if (it == null) { if (it == null) onComposed(null)
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
onComposed(null)
}
currentChatId.value = it currentChatId.value = it
} }
} }
} }
LaunchedEffect(Unit) {
snapshotFlow { ModalManager.center.modalCount.value > 0 }
.filter { chatModel.chatId.value == null }
.collect { modalBackground ->
if (chatModel.newChatSheetVisible.value) {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, appPrefs.oneHandUI.get())
} else if (modalBackground) {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false)
} else {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
}
}
}
Box(Modifier Box(Modifier
.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() } .then(if (hasCutout) Modifier.clip(RectangleShape) else Modifier)
.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp) .graphicsLayer { translationX = maxWidth.toPx() - minOf(offset.value.dp, maxWidth).toPx() }
) Box2@{ ) Box2@{
currentChatId.value?.let { currentChatId.value?.let {
ChatView(currentChatId, onComposed) ChatView(currentChatId, onComposed)
} }
} }
if (call != null && showCallArea) {
ActiveCallInteractiveArea(call)
}
} }
} }
@Composable @Composable
fun StartPartOfScreen(userPickerState: MutableStateFlow<AnimatedViewState>) { fun StartPartOfScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
if (chatModel.setDeliveryReceipts.value) { if (chatModel.setDeliveryReceipts.value) {
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
SetDeliveryReceiptsView(chatModel) SetDeliveryReceiptsView(chatModel)
}
} else { } else {
val stopped = chatModel.chatRunning.value == false val stopped = chatModel.chatRunning.value == false
if (chatModel.sharedContent.value == null) if (chatModel.sharedContent.value == null) {
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
ChatListView(chatModel, userPickerState, AppLock::setPerformLA, stopped) ChatListView(chatModel, userPickerState, AppLock::setPerformLA, stopped)
else }
} else {
// LALAL initial load of view doesn't show blur. Focusing text field shows it
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler(keyboardCoversBar = false)) {
ShareListView(chatModel, stopped) ShareListView(chatModel, stopped)
} }
} }
}
}
@Composable @Composable
fun CenterPartOfScreen() { fun CenterPartOfScreen() {
@@ -90,6 +90,9 @@ object ChatModel {
// Needed to check for bottom nav bar and to apply or not navigation bar color on Android // Needed to check for bottom nav bar and to apply or not navigation bar color on Android
val newChatSheetVisible = mutableStateOf(false) val newChatSheetVisible = mutableStateOf(false)
// Needed to apply black color to left/right cutout area on Android
val fullscreenGalleryVisible = mutableStateOf(false)
// preferences // preferences
val notificationPreviewMode by lazy { val notificationPreviewMode by lazy {
mutableStateOf( mutableStateOf(
@@ -118,6 +118,9 @@ class AppPreferences {
val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true) val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true)
val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true) val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true)
val privacyMediaBlurRadius = mkIntPreference(SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS, 0) val privacyMediaBlurRadius = mkIntPreference(SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS, 0)
// Blur broken on Android 12, see https://github.com/chrisbanes/haze/issues/77. And not available before 12
val deviceSupportsBlur = appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 32
val appearanceBarsBlurRadius = mkIntPreference(SHARED_PREFS_APPEARANCE_BARS_BLUR_RADIUS, if (deviceSupportsBlur) 50 else 0)
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false) val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false) val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false)
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null) val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
@@ -223,6 +226,8 @@ class AppPreferences {
val chatItemTail = mkBoolPreference(SHARED_PREFS_CHAT_ITEM_TAIL, true) val chatItemTail = mkBoolPreference(SHARED_PREFS_CHAT_ITEM_TAIL, true)
val fontScale = mkFloatPreference(SHARED_PREFS_FONT_SCALE, 1f) val fontScale = mkFloatPreference(SHARED_PREFS_FONT_SCALE, 1f)
val densityScale = mkFloatPreference(SHARED_PREFS_DENSITY_SCALE, 1f) val densityScale = mkFloatPreference(SHARED_PREFS_DENSITY_SCALE, 1f)
val inAppBarsDefaultAlpha = if (deviceSupportsBlur) 0.875f else 0.975f
val inAppBarsAlpha = mkFloatPreference(SHARED_PREFS_IN_APP_BARS_ALPHA, inAppBarsDefaultAlpha)
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null) val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0) val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0)
@@ -244,7 +249,7 @@ class AppPreferences {
val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true) val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true)
val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false) val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false)
val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, appPlatform.isAndroid) val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, true)
val hintPreferences: List<Pair<SharedPreference<Boolean>, Boolean>> = listOf( val hintPreferences: List<Pair<SharedPreference<Boolean>, Boolean>> = listOf(
laNoticeShown to false, laNoticeShown to false,
@@ -362,6 +367,7 @@ class AppPreferences {
private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles" private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles"
private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays" private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays"
private const val SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS = "PrivacyMediaBlurRadius" private const val SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS = "PrivacyMediaBlurRadius"
private const val SHARED_PREFS_APPEARANCE_BARS_BLUR_RADIUS = "AppearanceBarsBlurRadius"
const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup" const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup"
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls" private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites" private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites"
@@ -428,6 +434,7 @@ class AppPreferences {
private const val SHARED_PREFS_CHAT_ITEM_TAIL = "ChatItemTail" private const val SHARED_PREFS_CHAT_ITEM_TAIL = "ChatItemTail"
private const val SHARED_PREFS_FONT_SCALE = "FontScale" private const val SHARED_PREFS_FONT_SCALE = "FontScale"
private const val SHARED_PREFS_DENSITY_SCALE = "DensityScale" private const val SHARED_PREFS_DENSITY_SCALE = "DensityScale"
private const val SHARED_PREFS_IN_APP_BARS_ALPHA = "InAppBarsAlpha"
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"
private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime" private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime"
@@ -14,20 +14,11 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import java.io.File import java.io.File
expect fun Modifier.navigationBarsWithImePadding(): Modifier
@Composable
expect fun ProvideWindowInsets(
consumeWindowInsets: Boolean = true,
windowInsetsAnimationsEnabled: Boolean = true,
content: @Composable () -> Unit
)
@Composable @Composable
expect fun Modifier.desktopOnExternalDrag( expect fun Modifier.desktopOnExternalDrag(
enabled: Boolean = true, enabled: Boolean = true,
onFiles: (List<File>) -> Unit = {}, onFiles: (List<File>) -> Unit = {},
onImage: (Painter) -> Unit = {}, onImage: (File) -> Unit = {},
onText: (String) -> Unit = {} onText: (String) -> Unit = {}
): Modifier ): Modifier
@@ -8,6 +8,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import chat.simplex.common.model.ChatId import chat.simplex.common.model.ChatId
import chat.simplex.common.model.NotificationsMode import chat.simplex.common.model.NotificationsMode
import chat.simplex.common.ui.theme.CurrentColors
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
interface PlatformInterface { interface PlatformInterface {
@@ -20,12 +21,12 @@ interface PlatformInterface {
fun androidChatInitializedAndStarted() {} fun androidChatInitializedAndStarted() {}
fun androidIsBackgroundCallAllowed(): Boolean = true fun androidIsBackgroundCallAllowed(): Boolean = true
fun androidSetNightModeIfSupported() {} fun androidSetNightModeIfSupported() {}
fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {} fun androidSetStatusAndNavigationBarAppearance(isLightStatusBar: Boolean, isLightNavBar: Boolean, blackNavBar: Boolean = false, themeBackgroundColor: Color = CurrentColors.value.colors.background) {}
fun androidSetDrawerStatusAndNavBarColor(isLight: Boolean, drawerShadingColor: Color, toolbarOnTop: Boolean, navBarColor: Color) {}
fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {} fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {}
fun androidPictureInPictureAllowed(): Boolean = true fun androidPictureInPictureAllowed(): Boolean = true
fun androidCallEnded() {} fun androidCallEnded() {}
fun androidRestartNetworkObserver() {} fun androidRestartNetworkObserver() {}
val androidApiLevel: Int? get() = null
@Composable fun androidLockPortraitOrientation() {} @Composable fun androidLockPortraitOrientation() {}
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
@Composable fun desktopShowAppUpdateNotice() {} @Composable fun desktopShowAppUpdateNotice() {}
@@ -8,6 +8,7 @@ import androidx.compose.foundation.lazy.*
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
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@Composable @Composable
@@ -21,11 +22,44 @@ expect fun LazyColumnWithScrollBar(
horizontalAlignment: Alignment.Horizontal = Alignment.Start, horizontalAlignment: Alignment.Horizontal = Alignment.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true, userScrollEnabled: Boolean = true,
additionalBarOffset: State<Dp>? = null,
// by default, this function will include .fillMaxSize() without you doing anything. If you don't need it, pass `false` here
// maxSize (at least maxHeight) is needed for blur on appBars to work correctly
fillMaxSize: Boolean = true,
content: LazyListScope.() -> Unit
)
@Composable
expect fun LazyColumnWithScrollBarNoAppBar(
modifier: Modifier = Modifier,
state: LazyListState? = null,
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
additionalBarOffset: State<Dp>? = null,
content: LazyListScope.() -> Unit content: LazyListScope.() -> Unit
) )
@Composable @Composable
expect fun ColumnWithScrollBar( expect fun ColumnWithScrollBar(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
state: ScrollState? = null,
// set true when you want to show something in the center with respected .fillMaxSize()
maxIntrinsicSize: Boolean = false,
// by default, this function will include .fillMaxSize() without you doing anything. If you don't need it, pass `false` here
// maxSize (at least maxHeight) is needed for blur on appBars to work correctly
fillMaxSize: Boolean = true,
content: @Composable ColumnScope.() -> Unit
)
@Composable
expect fun ColumnWithScrollBarNoAppBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top, verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start, horizontalAlignment: Alignment.Horizontal = Alignment.Start,
@@ -1,14 +1,14 @@
package chat.simplex.common.ui.theme package chat.simplex.common.ui.theme
import androidx.compose.foundation.background
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
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.graphics.layer.GraphicsLayer
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.*
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ChatController import chat.simplex.common.model.ChatController
import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
@@ -587,20 +587,26 @@ data class ThemeModeOverride (
} }
} }
fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, shape: Shape = RectangleShape): Modifier { fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, bgLayerSize: MutableState<IntSize>?, bgLayer: GraphicsLayer?/*, shape: Shape = RectangleShape*/): Modifier {
return if (baseTheme == DefaultTheme.SIMPLEX) { return drawBehind {
this.background(brush = Brush.linearGradient( copyBackgroundToAppBar(bgLayerSize, bgLayer) {
if (baseTheme == DefaultTheme.SIMPLEX) {
drawRect(brush = themedBackgroundBrush())
} else {
drawRect(CurrentColors.value.colors.background)
}
}
}
}
fun themedBackgroundBrush(): Brush = Brush.linearGradient(
listOf( listOf(
CurrentColors.value.colors.background.darker(0.4f), CurrentColors.value.colors.background.darker(0.4f),
CurrentColors.value.colors.background.lighter(0.4f) CurrentColors.value.colors.background.lighter(0.4f)
), ),
Offset(0f, Float.POSITIVE_INFINITY), Offset(0f, Float.POSITIVE_INFINITY),
Offset(Float.POSITIVE_INFINITY, 0f) Offset(Float.POSITIVE_INFINITY, 0f)
), shape = shape) )
} else {
this.background(color = CurrentColors.value.colors.background, shape = shape)
}
}
val DEFAULT_PADDING = 20.dp val DEFAULT_PADDING = 20.dp
val DEFAULT_SPACE_AFTER_ICON = 4.dp val DEFAULT_SPACE_AFTER_ICON = 4.dp
@@ -1,7 +1,6 @@
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.material.MaterialTheme
import androidx.compose.runtime.MutableState 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
@@ -107,7 +106,7 @@ object ThemeManager {
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
platform.androidSetNightModeIfSupported() platform.androidSetNightModeIfSupported()
val c = CurrentColors.value.colors val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !ChatController.appPrefs.oneHandUI.get(), ChatController.appPrefs.oneHandUI.get()) platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight)
} }
fun changeDarkTheme(theme: String) { fun changeDarkTheme(theme: String) {
@@ -125,10 +124,6 @@ object ThemeManager {
themeIds[nonSystemThemeName] = prevValue.themeId themeIds[nonSystemThemeName] = prevValue.themeId
appPrefs.currentThemeIds.set(themeIds) appPrefs.currentThemeIds.set(themeIds)
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
if (name == ThemeColor.BACKGROUND) {
val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false)
}
} }
fun applyThemeColor(name: ThemeColor, color: Color? = null, pref: MutableState<ThemeModeOverride>) { fun applyThemeColor(name: ThemeColor, color: Color? = null, pref: MutableState<ThemeModeOverride>) {
@@ -7,40 +7,34 @@ import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.*
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.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.* import chat.simplex.common.views.chat.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import kotlinx.coroutines.flow.collect import chat.simplex.common.views.chat.item.CONSOLE_COMPOSE_LAYOUT_ID
import chat.simplex.common.views.chat.item.AdaptingBottomPaddingLayout
import chat.simplex.common.views.chatlist.NavigationBarBackground
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
fun TerminalView(floating: Boolean = false, close: () -> Unit) { fun TerminalView(floating: Boolean = false) {
val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) } val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }
val close = {
close()
if (appPlatform.isDesktop) {
ModalManager.center.closeModals()
}
}
BackHandler(onBack = {
close()
})
TerminalLayout( TerminalLayout(
composeState, composeState,
floating, floating,
sendCommand = { sendCommand(chatModel, composeState) }, sendCommand = { sendCommand(chatModel, composeState) },
close
) )
} }
@@ -69,7 +63,6 @@ fun TerminalLayout(
composeState: MutableState<ComposeState>, composeState: MutableState<ComposeState>,
floating: Boolean, floating: Boolean,
sendCommand: () -> Unit, sendCommand: () -> Unit,
close: () -> Unit
) { ) {
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
val textStyle = remember { mutableStateOf(smallFont) } val textStyle = remember { mutableStateOf(smallFont) }
@@ -77,12 +70,21 @@ fun TerminalLayout(
fun onMessageChange(s: String) { fun onMessageChange(s: String) {
composeState.value = composeState.value.copy(message = s) composeState.value = composeState.value.copy(message = s)
} }
val oneHandUI = remember { appPrefs.oneHandUI.state }
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { Box(Modifier.fillMaxSize()) {
Scaffold( val composeViewHeight = remember { mutableStateOf(0.dp) }
topBar = { CloseSheetBar(close) }, AdaptingBottomPaddingLayout(Modifier, CONSOLE_COMPOSE_LAYOUT_ID, composeViewHeight) {
bottomBar = { TerminalLog(floating, composeViewHeight)
Column { Column(
Modifier
.layoutId(CONSOLE_COMPOSE_LAYOUT_ID)
.align(Alignment.BottomCenter)
.navigationBarsPadding()
.consumeWindowInsets(PaddingValues(bottom = if (oneHandUI.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp))
.imePadding()
.padding(bottom = if (oneHandUI.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp)
.background(MaterialTheme.colors.background)
) {
Divider() Divider()
Box(Modifier.padding(horizontal = 8.dp)) { Box(Modifier.padding(horizontal = 8.dp)) {
SendMsgView( SendMsgView(
@@ -110,32 +112,21 @@ fun TerminalLayout(
) )
} }
} }
},
contentColor = LocalContentColor.current,
modifier = Modifier.navigationBarsWithImePadding()
) { contentPadding ->
Surface(
modifier = Modifier
.padding(contentPadding)
.fillMaxWidth(),
color = MaterialTheme.colors.background,
contentColor = LocalContentColor.current
) {
TerminalLog(floating)
} }
if (!oneHandUI.value) {
NavigationBarBackground(true, oneHandUI.value)
} }
} }
} }
@Composable @Composable
fun TerminalLog(floating: Boolean) { fun TerminalLog(floating: Boolean, composeViewHeight: State<Dp>) {
val reversedTerminalItems by remember { val reversedTerminalItems by remember {
derivedStateOf { chatModel.terminalItems.value.asReversed() } derivedStateOf { chatModel.terminalItems.value.asReversed() }
} }
val clipboard = LocalClipboardManager.current
val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState() val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
var autoScrollToBottom = true var autoScrollToBottom = listState.firstVisibleItemIndex <= 1
launch { launch {
snapshotFlow { listState.layoutInfo.totalItemsCount } snapshotFlow { listState.layoutInfo.totalItemsCount }
.filter { autoScrollToBottom } .filter { autoScrollToBottom }
@@ -150,12 +141,21 @@ fun TerminalLog(floating: Boolean) {
launch { launch {
snapshotFlow { listState.firstVisibleItemIndex } snapshotFlow { listState.firstVisibleItemIndex }
.collect { .collect {
autoScrollToBottom = listState.firstVisibleItemIndex == 0 autoScrollToBottom = it == 0
} }
} }
} }
LazyColumnWithScrollBar(reverseLayout = true, state = listState) { LazyColumnWithScrollBar (
reverseLayout = true,
contentPadding = PaddingValues(
top = topPaddingToContent(),
bottom = composeViewHeight.value
),
state = listState,
additionalBarOffset = composeViewHeight
) {
items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item -> items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item ->
val clipboard = LocalClipboardManager.current
val rhId = item.remoteHostId val rhId = item.remoteHostId
val rhIdStr = if (rhId == null) "" else "$rhId " val rhIdStr = if (rhId == null) "" else "$rhId "
Text( Text(
@@ -172,7 +172,8 @@ fun TerminalLog(floating: Boolean) {
ModalManager.start ModalManager.start
} }
modalPlace.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) { modalPlace.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) {
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) { ColumnWithScrollBar {
SelectionContainer {
val details = item.details val details = item.details
.let { .let {
if (it.length < 100_000) it if (it.length < 100_000) it
@@ -181,6 +182,7 @@ fun TerminalLog(floating: Boolean) {
Text(details, modifier = Modifier.heightIn(max = 50_000.dp).padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING)) Text(details, modifier = Modifier.heightIn(max = 50_000.dp).padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
} }
} }
}
}.padding(horizontal = 8.dp, vertical = 4.dp) }.padding(horizontal = 8.dp, vertical = 4.dp)
) )
} }
@@ -208,8 +210,7 @@ fun PreviewTerminalLayout() {
TerminalLayout( TerminalLayout(
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) },
sendCommand = {}, sendCommand = {},
floating = false, floating = false
close = {}
) )
} }
} }
@@ -40,8 +40,6 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val keyboardState by getKeyboardState() val keyboardState by getKeyboardState()
var savedKeyboardState by remember { mutableStateOf(keyboardState) } var savedKeyboardState by remember { mutableStateOf(keyboardState) }
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -50,11 +48,9 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
val displayName = rememberSaveable { mutableStateOf("") } val displayName = rememberSaveable { mutableStateOf("") }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
ColumnWithScrollBar( ColumnWithScrollBar {
modifier = Modifier.fillMaxSize()
) {
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING) AppBarTitle(stringResource(MR.strings.create_profile), withPadding = false, bottomPadding = DEFAULT_PADDING)
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text( Text(
stringResource(MR.strings.display_name), stringResource(MR.strings.display_name),
@@ -103,7 +99,6 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
} }
} }
} }
}
@Composable @Composable
fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) { fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) {
@@ -111,31 +106,15 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val keyboardState by getKeyboardState() val keyboardState by getKeyboardState()
var savedKeyboardState by remember { mutableStateOf(keyboardState) } var savedKeyboardState by remember { mutableStateOf(keyboardState) }
val handler = remember { AppBarHandler() } CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
CompositionLocalProvider( ModalView({
LocalAppBarHandler provides handler
) {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Column(
modifier = Modifier
.fillMaxSize()
.themedBackground(),
horizontalAlignment = Alignment.CenterHorizontally
) {
CloseSheetBar(close = {
if (chatModel.users.none { !it.user.hidden }) { if (chatModel.users.none { !it.user.hidden }) {
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
} else { } else {
close() close()
} }
}) }) {
BackHandler(onBack = { ColumnWithScrollBar {
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
})
ColumnWithScrollBar(
modifier = Modifier.fillMaxSize()
) {
val displayName = rememberSaveable { mutableStateOf("") } val displayName = rememberSaveable { mutableStateOf("") }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
Column(if (appPlatform.isAndroid) Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING) else Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally)) { Column(if (appPlatform.isAndroid) Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING) else Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally)) {
@@ -165,7 +144,6 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) {
focusRequester.requestFocus() focusRequester.requestFocus()
} }
} }
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
setLastVersionDefault(chatModel) setLastVersionDefault(chatModel)
} }
@@ -255,7 +233,6 @@ fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isVal
val modifier = Modifier val modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = 50.dp) .heightIn(min = 50.dp)
.navigationBarsWithImePadding()
.onFocusChanged { focused = it.isFocused } .onFocusChanged { focused = it.isFocused }
Column( Column(
Modifier Modifier
@@ -289,6 +266,7 @@ fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isVal
enabled = true, enabled = true,
isError = false, isError = false,
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified)
) )
} }
) )
@@ -53,7 +53,7 @@ fun IncomingCallAlertLayout(
acceptCall: () -> Unit acceptCall: () -> Unit
) { ) {
val color = if (isInDarkTheme()) MaterialTheme.colors.surface else IncomingCallLight val color = if (isInDarkTheme()) MaterialTheme.colors.surface else IncomingCallLight
Column(Modifier.fillMaxWidth().background(color).padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) { Column(Modifier.fillMaxWidth().background(color).statusBarsPadding().padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) {
IncomingCallInfo(invitation, chatModel) IncomingCallInfo(invitation, chatModel)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
@@ -529,10 +529,7 @@ fun ChatInfoLayout(
KeyChangeEffect(chat.id) { KeyChangeEffect(chat.id) {
scope.launch { scrollState.scrollTo(0) } scope.launch { scrollState.scrollTo(0) }
} }
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier
.fillMaxWidth()
) {
Row( Row(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
@@ -276,7 +276,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
@Composable @Composable
fun HistoryTab() { fun HistoryTab() {
ColumnWithScrollBar(Modifier.fillMaxWidth()) { ColumnWithScrollBar {
Details() Details()
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
val versions = ciInfo.itemVersions val versions = ciInfo.itemVersions
@@ -300,7 +300,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
@Composable @Composable
fun QuoteTab(qi: CIQuote) { fun QuoteTab(qi: CIQuote) {
ColumnWithScrollBar(Modifier.fillMaxWidth()) { ColumnWithScrollBar {
Details() Details()
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
@@ -313,7 +313,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
@Composable @Composable
fun ForwardedFromTab(forwardedFromItem: AChatItem) { fun ForwardedFromTab(forwardedFromItem: AChatItem) {
ColumnWithScrollBar(Modifier.fillMaxWidth()) { ColumnWithScrollBar {
Details() Details()
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
SectionView { SectionView {
@@ -375,7 +375,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
@Composable @Composable
fun DeliveryTab(memberDeliveryStatuses: List<MemberDeliveryStatus>) { fun DeliveryTab(memberDeliveryStatuses: List<MemberDeliveryStatus>) {
ColumnWithScrollBar(Modifier.fillMaxWidth()) { ColumnWithScrollBar {
Details() Details()
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
val mss = membersStatuses(chatModel, memberDeliveryStatuses) val mss = membersStatuses(chatModel, memberDeliveryStatuses)
@@ -12,10 +12,11 @@ import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
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.*
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.layout.layoutId import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.layout.onSizeChanged
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
@@ -108,6 +109,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
} }
} }
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler(chatInfo.id, keyboardCoversBar = false)) {
when (chatInfo) { when (chatInfo) {
is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> { is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> {
val perChatTheme = remember(chatInfo, CurrentColors.value.base) { if (chatInfo is ChatInfo.Direct) chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null } val perChatTheme = remember(chatInfo, CurrentColors.value.base) { if (chatInfo is ChatInfo.Direct) chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null }
@@ -523,28 +525,10 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(), showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(),
showSearch = showSearch showSearch = showSearch
) )
if (appPlatform.isAndroid) {
val backgroundColor = MaterialTheme.colors.background
val backgroundColorState = rememberUpdatedState(backgroundColor)
LaunchedEffect(Unit) {
snapshotFlow { ModalManager.center.modalCount.value > 0 }
.collect { modalBackground ->
if (modalBackground) {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false)
} else {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, backgroundColorState.value, true, false)
}
}
}
}
} }
} }
is ChatInfo.ContactConnection -> { is ChatInfo.ContactConnection -> {
val close = { chatModel.chatId.value = null } val close = { chatModel.chatId.value = null }
val handler = remember { AppBarHandler() }
CompositionLocalProvider(
LocalAppBarHandler provides handler
) {
ModalView(close, showClose = appPlatform.isAndroid, content = { ModalView(close, showClose = appPlatform.isAndroid, content = {
ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, false, close) ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, false, close)
}) })
@@ -554,13 +538,8 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
chatModel.chatItems.clear() chatModel.chatItems.clear()
} }
} }
}
is ChatInfo.InvalidJSON -> { is ChatInfo.InvalidJSON -> {
val close = { chatModel.chatId.value = null } val close = { chatModel.chatId.value = null }
val handler = remember { AppBarHandler() }
CompositionLocalProvider(
LocalAppBarHandler provides handler
) {
ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chatInfo.json) } }, content = { ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chatInfo.json) } }, content = {
InvalidJSONView(chatInfo.json) InvalidJSONView(chatInfo.json)
}) })
@@ -570,11 +549,11 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
chatModel.chatItems.clear() chatModel.chatItems.clear()
} }
} }
}
else -> {} else -> {}
} }
} }
} }
}
fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType) { fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType) {
withBGApi { withBGApi {
@@ -642,24 +621,15 @@ fun ChatLayout(
.desktopOnExternalDrag( .desktopOnExternalDrag(
enabled = remember(attachmentDisabled.value, chatInfo.value?.userCanSend) { mutableStateOf(!attachmentDisabled.value && chatInfo.value?.userCanSend == true) }.value, enabled = remember(attachmentDisabled.value, chatInfo.value?.userCanSend) { mutableStateOf(!attachmentDisabled.value && chatInfo.value?.userCanSend == true) }.value,
onFiles = { paths -> composeState.onFilesAttached(paths.map { it.toURI() }) }, onFiles = { paths -> composeState.onFilesAttached(paths.map { it.toURI() }) },
onImage = { onImage = { file -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(file.toURI()), null) } },
// TODO: file is not saved anywhere?!
val tmpFile = File.createTempFile("image", ".bmp", tmpDir)
tmpFile.deleteOnExit()
chatModel.filesToDelete.add(tmpFile)
val uri = tmpFile.toURI()
CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(uri), null) }
},
onText = { onText = {
// Need to parse HTML in order to correctly display the content // Need to parse HTML in order to correctly display the content
//composeState.value = composeState.value.copy(message = composeState.value.message + it) //composeState.value = composeState.value.copy(message = composeState.value.message + it)
}, },
) )
) { ) {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
ModalBottomSheetLayout( ModalBottomSheetLayout(
scrimColor = Color.Black.copy(alpha = 0.12F), scrimColor = Color.Black.copy(alpha = 0.12F),
modifier = Modifier.navigationBarsWithImePadding(),
sheetElevation = 0.dp, sheetElevation = 0.dp,
sheetContent = { sheetContent = {
ChooseAttachmentView( ChooseAttachmentView(
@@ -670,53 +640,48 @@ fun ChatLayout(
sheetState = attachmentBottomSheetState, sheetState = attachmentBottomSheetState,
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
) { ) {
val floatingButton: MutableState<@Composable () -> Unit> = remember { mutableStateOf({}) } val composeViewHeight = remember { mutableStateOf(0.dp) }
val setFloatingButton = { button: @Composable () -> Unit -> Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer)) {
floatingButton.value = button val remoteHostId = remember { remoteHostId }.value
val chatInfo = remember { chatInfo }.value
AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) {
if (chatInfo != null) {
Box(Modifier.fillMaxSize()) {
ChatItemsList(
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markRead, remember { { onComposed(it) } }, developerTools, showViaProxy,
)
} }
}
Scaffold( val oneHandUI = remember { appPrefs.oneHandUI.state }
topBar = { Box(
Modifier
.layoutId(CHAT_COMPOSE_LAYOUT_ID)
.align(Alignment.BottomCenter)
.imePadding()
.navigationBarsPadding()
.then(if (oneHandUI.value) Modifier.padding(bottom = AppBarHeight * fontSizeSqrtMultiplier) else Modifier)
) {
composeView()
}
}
val oneHandUI = remember { appPrefs.oneHandUI.state }
if (oneHandUI.value) {
StatusBarBackground()
} else {
NavigationBarBackground(true, oneHandUI.value, noAlpha = true)
}
Box(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) {
if (selectedChatItems.value == null) { if (selectedChatItems.value == null) {
val chatInfo = chatInfo.value
if (chatInfo != null) { if (chatInfo != null) {
ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
} }
} else { } else {
SelectedItemsTopToolbar(selectedChatItems) SelectedItemsTopToolbar(selectedChatItems)
} }
},
bottomBar = composeView,
modifier = Modifier.navigationBarsWithImePadding(),
floatingActionButton = { floatingButton.value() },
contentColor = LocalContentColor.current,
backgroundColor = Color.Unspecified
) { 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
.fillMaxSize()
.background(MaterialTheme.colors.background)
.then(if (wallpaperImage != null)
Modifier.drawWithCache { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) }
else
Modifier)
.padding(contentPadding)
) {
val remoteHostId = remember { remoteHostId }.value
val chatInfo = remember { chatInfo }.value
if (chatInfo != null) {
ChatItemsList(
remoteHostId, chatInfo, unreadCount, composeState, searchValue,
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy,
)
}
}
} }
} }
} }
@@ -724,7 +689,7 @@ fun ChatLayout(
} }
@Composable @Composable
fun ChatInfoToolbar( fun BoxScope.ChatInfoToolbar(
chatInfo: ChatInfo, chatInfo: ChatInfo,
back: () -> Unit, back: () -> Unit,
info: () -> Unit, info: () -> Unit,
@@ -876,24 +841,36 @@ fun ChatInfoToolbar(
} }
} }
} }
val oneHandUI = remember { appPrefs.oneHandUI.state }
DefaultTopAppBar( DefaultAppBar(
navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } }, navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } },
title = { ChatInfoToolbarTitle(chatInfo) }, title = { ChatInfoToolbarTitle(chatInfo) },
onTitleClick = if (chatInfo is ChatInfo.Local) null else info, onTitleClick = if (chatInfo is ChatInfo.Local) null else info,
showSearch = showSearch.value, showSearch = showSearch.value,
onTop = !oneHandUI.value,
onSearchValueChanged = onSearchValueChanged, onSearchValueChanged = onSearchValueChanged,
buttons = barButtons buttons = { barButtons.forEach { it() } }
) )
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) {
Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier)) val density = LocalDensity.current
val width = remember { mutableStateOf(250.dp) }
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight * fontSizeSqrtMultiplier)) { val height = remember { mutableStateOf(0.dp) }
DefaultDropdownMenu(showMenu) { DefaultDropdownMenu(
showMenu,
modifier = Modifier.onSizeChanged { with(density) {
width.value = it.width.toDp().coerceAtLeast(250.dp)
if (oneHandUI.value && (appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 30)) height.value = it.height.toDp()
} },
offset = DpOffset(-width.value, if (oneHandUI.value) -height.value else AppBarHeight)
) {
if (oneHandUI.value) {
menuItems.asReversed().forEach { it() }
} else {
menuItems.forEach { it() } menuItems.forEach { it() }
} }
} }
} }
}
@Composable @Composable
fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f)) { fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f)) {
@@ -934,11 +911,12 @@ private fun ContactVerifiedShield() {
} }
@Composable @Composable
fun BoxWithConstraintsScope.ChatItemsList( fun BoxScope.ChatItemsList(
remoteHostId: Long?, remoteHostId: Long?,
chatInfo: ChatInfo, chatInfo: ChatInfo,
unreadCount: State<Int>, unreadCount: State<Int>,
composeState: MutableState<ComposeState>, composeState: MutableState<ComposeState>,
composeViewHeight: State<Dp>,
searchValue: State<String>, searchValue: State<String>,
useLinkPreviews: Boolean, useLinkPreviews: Boolean,
linkMode: SimplexLinkMode, linkMode: SimplexLinkMode,
@@ -963,7 +941,6 @@ fun BoxWithConstraintsScope.ChatItemsList(
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
showItemDetails: (ChatInfo, ChatItem) -> Unit, showItemDetails: (ChatInfo, ChatItem) -> Unit,
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
setFloatingButton: (@Composable () -> Unit) -> Unit,
onComposed: suspend (chatId: String) -> Unit, onComposed: suspend (chatId: String) -> Unit,
developerTools: Boolean, developerTools: Boolean,
showViaProxy: Boolean showViaProxy: Boolean
@@ -987,14 +964,19 @@ fun BoxWithConstraintsScope.ChatItemsList(
PreloadItems(chatInfo.id, listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages) PreloadItems(chatInfo.id, listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages)
Spacer(Modifier.size(8.dp)) Spacer(Modifier.size(8.dp))
val reversedChatItems by remember { derivedStateOf { chatModel.chatItems.asReversed() } } val reversedChatItems = remember { derivedStateOf { chatModel.chatItems.asReversed() } }
val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() } val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent().roundToPx() })
val scrollToItem: (Long) -> Unit = { itemId: Long -> val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } }
val index = reversedChatItems.indexOfFirst { it.id == itemId } val scrollToItem: State<(Long) -> Unit> = remember {
mutableStateOf(
{ itemId: Long ->
val index = reversedChatItems.value.indexOfFirst { it.id == itemId }
if (index != -1) { if (index != -1) {
scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.lastIndex, index + 1), -maxHeightRounded) } scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.value.lastIndex, index + 1), -maxHeight.value) }
} }
} }
)
}
// TODO: Having this block on desktop makes ChatItemsList() to recompose twice on chatModel.chatId update instead of once // TODO: Having this block on desktop makes ChatItemsList() to recompose twice on chatModel.chatId update instead of once
LaunchedEffect(chatInfo.id) { LaunchedEffect(chatInfo.id) {
var stopListening = false var stopListening = false
@@ -1011,8 +993,18 @@ fun BoxWithConstraintsScope.ChatItemsList(
VideoPlayerHolder.releaseAll() VideoPlayerHolder.releaseAll()
} }
) )
LazyColumnWithScrollBar(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) { LazyColumnWithScrollBar(
itemsIndexed(reversedChatItems, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem -> Modifier.align(Alignment.BottomCenter),
state = listState,
reverseLayout = true,
contentPadding = PaddingValues(
top = topPaddingToContent(),
bottom = composeViewHeight.value
),
additionalBarOffset = composeViewHeight
) {
itemsIndexed(reversedChatItems.value, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem ->
val itemScope = rememberCoroutineScope()
CompositionLocalProvider( CompositionLocalProvider(
// Makes horizontal and vertical scrolling to coexist nicely. // Makes horizontal and vertical scrolling to coexist nicely.
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
@@ -1020,10 +1012,10 @@ fun BoxWithConstraintsScope.ChatItemsList(
) { ) {
val provider = { val provider = {
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed -> providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
scope.launch { itemScope.launch {
listState.scrollToItem( listState.scrollToItem(
kotlin.math.min(reversedChatItems.lastIndex, indexInReversed + 1), kotlin.math.min(reversedChatItems.value.lastIndex, indexInReversed + 1),
-maxHeightRounded -maxHeight.value
) )
} }
} }
@@ -1036,7 +1028,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(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, 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, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, 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.value, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
} }
} }
@@ -1044,7 +1036,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?, itemSeparation: ItemSeparation, previousItemSeparation: ItemSeparation?) { fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?, itemSeparation: ItemSeparation, previousItemSeparation: ItemSeparation?) {
val dismissState = rememberDismissState(initialValue = DismissValue.Default) { val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
if (it == DismissValue.DismissedToStart) { if (it == DismissValue.DismissedToStart) {
scope.launch { itemScope.launch {
if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) { if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) {
if (composeState.value.editing) { if (composeState.value.editing) {
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
@@ -1241,16 +1233,17 @@ fun BoxWithConstraintsScope.ChatItemsList(
} }
val range = chatViewItemsRange(currIndex, prevHidden) val range = chatViewItemsRange(currIndex, prevHidden)
val reversed = reversedChatItems.value
if (revealed.value && range != null) { if (revealed.value && range != null) {
reversedChatItems.subList(range.first, range.last + 1).forEachIndexed { index, ci -> reversed.subList(range.first, range.last + 1).forEachIndexed { index, ci ->
val prev = if (index + range.first == prevHidden) prevItem else reversedChatItems[index + range.first + 1] val prev = if (index + range.first == prevHidden) prevItem else reversed[index + range.first + 1]
ChatItemView(ci, null, prev, itemSeparation, previousItemSeparation) ChatItemView(ci, null, prev, itemSeparation, previousItemSeparation)
} }
} else { } else {
ChatItemView(cItem, range, prevItem, itemSeparation, previousItemSeparation) ChatItemView(cItem, range, prevItem, itemSeparation, previousItemSeparation)
} }
if (i == reversedChatItems.lastIndex) { if (i == reversed.lastIndex) {
DateSeparator(cItem.meta.itemTs) DateSeparator(cItem.meta.itemTs)
} }
} }
@@ -1258,7 +1251,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) { if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) {
LaunchedEffect(cItem.id) { LaunchedEffect(cItem.id) {
scope.launch { itemScope.launch {
delay(600) delay(600)
markRead(CC.ItemRange(cItem.id, cItem.id), null) markRead(CC.ItemRange(cItem.id, cItem.id), null)
} }
@@ -1267,10 +1260,10 @@ fun BoxWithConstraintsScope.ChatItemsList(
} }
} }
} }
FloatingButtons(chatModel.chatItems, unreadCount, remoteHostId, chatInfo, searchValue, markRead, setFloatingButton, listState) FloatingButtons(chatModel.chatItems, unreadCount, composeViewHeight, remoteHostId, chatInfo, searchValue, markRead, listState)
FloatingDate( FloatingDate(
Modifier.padding(top = 10.dp).align(Alignment.TopCenter), Modifier.padding(top = 10.dp + topPaddingToContent()).align(Alignment.TopCenter),
listState, listState,
) )
@@ -1325,87 +1318,65 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
} }
@Composable @Composable
fun BoxWithConstraintsScope.FloatingButtons( fun BoxScope.FloatingButtons(
chatItems: State<List<ChatItem>>, chatItems: State<List<ChatItem>>,
unreadCount: State<Int>, unreadCount: State<Int>,
composeViewHeight: State<Dp>,
remoteHostId: Long?, remoteHostId: Long?,
chatInfo: ChatInfo, chatInfo: ChatInfo,
searchValue: State<String>, searchValue: State<String>,
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
setFloatingButton: (@Composable () -> Unit) -> Unit,
listState: LazyListState listState: LazyListState
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var firstVisibleIndex by remember { mutableStateOf(listState.firstVisibleItemIndex) } val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportSize.height } }
var lastIndexOfVisibleItems by remember { mutableStateOf(listState.layoutInfo.visibleItemsInfo.lastIndex) } val bottomUnreadCount = remember {
var firstItemIsVisible by remember { mutableStateOf(firstVisibleIndex == 0) }
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.distinctUntilChanged()
.collect {
firstVisibleIndex = it
firstItemIsVisible = firstVisibleIndex == 0
}
}
LaunchedEffect(listState) {
// When both snapshotFlows located in one LaunchedEffect second block will never be called because coroutine is paused on first block
// so separate them into two LaunchedEffects
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastIndex }
.distinctUntilChanged()
.collect {
lastIndexOfVisibleItems = it
}
}
val bottomUnreadCount by remember {
derivedStateOf { derivedStateOf {
if (unreadCount.value == 0) return@derivedStateOf 0 if (unreadCount.value == 0) return@derivedStateOf 0
val items = chatItems.value val items = chatItems.value
val from = items.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems val from = items.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex
if (items.size <= from || from < 0) return@derivedStateOf 0 if (items.size <= from || from < 0) return@derivedStateOf 0
items.subList(from, items.size).count { it.isRcvNew } items.subList(from, items.size).count { it.isRcvNew }
} }
} }
val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt()
LaunchedEffect(bottomUnreadCount, firstItemIsVisible) { val showBottomButtonWithCounter = remember { derivedStateOf { bottomUnreadCount.value > 0 && listState.firstVisibleItemIndex != 0 && searchValue.value.isEmpty() } }
val showButtonWithCounter = bottomUnreadCount > 0 && !firstItemIsVisible && searchValue.value.isEmpty() val showBottomButtonWithArrow = remember { derivedStateOf { !showBottomButtonWithCounter.value && listState.firstVisibleItemIndex != 0 } }
val showButtonWithArrow = !showButtonWithCounter && !firstItemIsVisible BottomEndFloatingButton(
setFloatingButton(
bottomEndFloatingButton(
bottomUnreadCount, bottomUnreadCount,
showButtonWithCounter, showBottomButtonWithCounter,
showButtonWithArrow, showBottomButtonWithArrow,
composeViewHeight,
onClickArrowDown = { onClickArrowDown = {
scope.launch { listState.animateScrollToItem(0) } scope.launch { listState.animateScrollToItem(0) }
}, },
onClickCounter = { onClickCounter = {
scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount - 1), firstVisibleOffset) } val firstVisibleOffset = (-maxHeight.value * 0.8).toInt()
} scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount.value - 1), firstVisibleOffset) }
))
} }
)
// Don't show top FAB if is in search // Don't show top FAB if is in search
if (searchValue.value.isNotEmpty()) return if (searchValue.value.isNotEmpty()) return
val fabSize = 56.dp val fabSize = 56.dp
val topUnreadCount by remember { val topUnreadCount = remember { derivedStateOf { unreadCount.value - bottomUnreadCount.value } }
derivedStateOf { unreadCount.value - bottomUnreadCount }
}
val showButtonWithCounter = topUnreadCount > 0
val height = with(LocalDensity.current) { maxHeight.toPx() }
val showDropDown = remember { mutableStateOf(false) } val showDropDown = remember { mutableStateOf(false) }
TopEndFloatingButton( TopEndFloatingButton(
Modifier.padding(end = DEFAULT_PADDING, top = 24.dp).align(Alignment.TopEnd), Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent()).align(Alignment.TopEnd),
topUnreadCount, topUnreadCount,
showButtonWithCounter, onClick = { scope.launch { listState.animateScrollBy(maxHeight.value.toFloat()) } },
onClick = { scope.launch { listState.animateScrollBy(height) } },
onLongClick = { showDropDown.value = true } onLongClick = { showDropDown.value = true }
) )
Box { Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) {
DefaultDropdownMenu(showDropDown, offset = DpOffset(this@FloatingButtons.maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) { val density = LocalDensity.current
val width = remember { mutableStateOf(250.dp) }
DefaultDropdownMenu(
showDropDown,
modifier = Modifier.onSizeChanged { with(density) { width.value = it.width.toDp().coerceAtLeast(250.dp) } },
offset = DpOffset(-DEFAULT_PADDING - width.value, 24.dp + fabSize + topPaddingToContent())
) {
ItemAction( ItemAction(
generalGetString(MR.strings.mark_read), generalGetString(MR.strings.mark_read),
painterResource(MR.images.ic_check), painterResource(MR.images.ic_check),
@@ -1413,7 +1384,7 @@ fun BoxWithConstraintsScope.FloatingButtons(
val minUnreadItemId = chatModel.chats.value.firstOrNull { it.remoteHostId == remoteHostId && it.id == chatInfo.id }?.chatStats?.minUnreadItemId ?: return@ItemAction val minUnreadItemId = chatModel.chats.value.firstOrNull { it.remoteHostId == remoteHostId && it.id == chatInfo.id }?.chatStats?.minUnreadItemId ?: return@ItemAction
markRead( markRead(
CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.value.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.value.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
bottomUnreadCount bottomUnreadCount.value
) )
showDropDown.value = false showDropDown.value = false
}) })
@@ -1475,12 +1446,11 @@ fun MemberImage(member: GroupMember) {
@Composable @Composable
private fun TopEndFloatingButton( private fun TopEndFloatingButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
unreadCount: Int, unreadCount: State<Int>,
showButtonWithCounter: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit onLongClick: () -> Unit
) = when { ) = when {
showButtonWithCounter -> { unreadCount.value > 0 -> {
val interactionSource = interactionSourceWithDetection(onClick, onLongClick) val interactionSource = interactionSourceWithDetection(onClick, onLongClick)
FloatingActionButton( FloatingActionButton(
{}, // no action here {}, // no action here
@@ -1490,7 +1460,7 @@ private fun TopEndFloatingButton(
interactionSource = interactionSource, interactionSource = interactionSource,
) { ) {
Text( Text(
unreadCountStr(unreadCount), unreadCountStr(unreadCount.value),
color = MaterialTheme.colors.primary, color = MaterialTheme.colors.primary,
fontSize = 14.sp, fontSize = 14.sp,
) )
@@ -1500,6 +1470,16 @@ private fun TopEndFloatingButton(
} }
} }
@Composable
fun topPaddingToContent(): Dp {
val oneHandUI = remember { appPrefs.oneHandUI.state }
return if (oneHandUI.value) {
WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
} else {
AppBarHeight * fontSizeSqrtMultiplier + WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
}
}
@Composable @Composable
private fun FloatingDate( private fun FloatingDate(
modifier: Modifier, modifier: Modifier,
@@ -1509,8 +1489,9 @@ private fun FloatingDate(
var isNearBottom by remember { mutableStateOf(true) } var isNearBottom by remember { mutableStateOf(true) }
val lastVisibleItemDate = remember { val lastVisibleItemDate = remember {
derivedStateOf { derivedStateOf {
if (listState.layoutInfo.visibleItemsInfo.lastIndex >= 0 && listState.firstVisibleItemIndex >= 0) { if (listState.layoutInfo.visibleItemsInfo.lastIndex >= 0) {
val lastVisibleChatItemIndex = chatModel.chatItems.value.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex val lastFullyVisibleOffset = listState.layoutInfo.viewportEndOffset
val lastVisibleChatItemIndex = chatModel.chatItems.value.lastIndex - (listState.layoutInfo.visibleItemsInfo.lastOrNull { item -> item.offset + item.size <= lastFullyVisibleOffset && item.size > 0 }?.index ?: 0)
val item = chatModel.chatItems.value.getOrNull(lastVisibleChatItemIndex) val item = chatModel.chatItems.value.getOrNull(lastVisibleChatItemIndex)
val timeZone = TimeZone.currentSystemDefault() val timeZone = TimeZone.currentSystemDefault()
item?.meta?.itemTs?.toLocalDateTime(timeZone)?.date?.atStartOfDayIn(timeZone) item?.meta?.itemTs?.toLocalDateTime(timeZone)?.date?.atStartOfDayIn(timeZone)
@@ -1697,35 +1678,34 @@ fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (
} }
} }
private fun bottomEndFloatingButton( @Composable
unreadCount: Int, private fun BoxScope.BottomEndFloatingButton(
showButtonWithCounter: Boolean, unreadCount: State<Int>,
showButtonWithArrow: Boolean, showButtonWithCounter: State<Boolean>,
showButtonWithArrow: State<Boolean>,
composeViewHeight: State<Dp>,
onClickArrowDown: () -> Unit, onClickArrowDown: () -> Unit,
onClickCounter: () -> Unit onClickCounter: () -> Unit
): @Composable () -> Unit = when { ) = when {
showButtonWithCounter -> { showButtonWithCounter.value -> {
{
FloatingActionButton( FloatingActionButton(
onClick = onClickCounter, onClick = onClickCounter,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
modifier = Modifier.size(48.dp), modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp),
backgroundColor = MaterialTheme.colors.secondaryVariant, backgroundColor = MaterialTheme.colors.secondaryVariant,
) { ) {
Text( Text(
unreadCountStr(unreadCount), unreadCountStr(unreadCount.value),
color = MaterialTheme.colors.primary, color = MaterialTheme.colors.primary,
fontSize = 14.sp, fontSize = 14.sp,
) )
} }
} }
} showButtonWithArrow.value -> {
showButtonWithArrow -> {
{
FloatingActionButton( FloatingActionButton(
onClick = onClickArrowDown, onClick = onClickArrowDown,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
modifier = Modifier.size(48.dp), modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp),
backgroundColor = MaterialTheme.colors.secondaryVariant, backgroundColor = MaterialTheme.colors.secondaryVariant,
) { ) {
Icon( Icon(
@@ -1735,10 +1715,7 @@ private fun bottomEndFloatingButton(
) )
} }
} }
} else -> {}
else -> {
{}
}
} }
@Composable @Composable
@@ -1865,6 +1842,25 @@ private fun memberNames(member: GroupMember, prevMember: GroupMember?, memCount:
} }
} }
fun Modifier.chatViewBackgroundModifier(
colors: Colors,
wallpaper: AppWallpaper,
backgroundGraphicsLayerSize: MutableState<IntSize>?,
backgroundGraphicsLayer: GraphicsLayer?
): Modifier {
val wallpaperImage = wallpaper.type.image
val wallpaperType = wallpaper.type
val backgroundColor = wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, colors.background)
val tintColor = wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base)
return this
.then(if (wallpaperImage != null)
Modifier.drawWithCache { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor, backgroundGraphicsLayerSize, backgroundGraphicsLayer) }
else
Modifier.drawWithCache { onDrawBehind { copyBackgroundToAppBar(backgroundGraphicsLayerSize, backgroundGraphicsLayer) { drawRect(backgroundColor) } } }
)
}
fun chatViewItemsRange(currIndex: Int?, prevHidden: Int?): IntRange? = fun chatViewItemsRange(currIndex: Int?, prevHidden: Int?): IntRange? =
if (currIndex != null && prevHidden != null && prevHidden > currIndex) { if (currIndex != null && prevHidden != null && prevHidden > currIndex) {
currIndex..prevHidden currIndex..prevHidden
@@ -13,12 +13,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
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.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.model.ChatModel.filesToDelete import chat.simplex.common.model.ChatModel.filesToDelete
import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.model.ChatModel.withChats
@@ -896,7 +898,7 @@ fun ComposeView(
} }
} }
} }
Column(Modifier.background(MaterialTheme.colors.background)) { Box(Modifier.background(MaterialTheme.colors.background)) {
Divider() Divider()
Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) { Row(Modifier.padding(end = 8.dp), 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)
@@ -918,7 +920,7 @@ fun ComposeView(
&& !nextSendGrpInv.value && !nextSendGrpInv.value
IconButton( IconButton(
attachmentClicked, attachmentClicked,
Modifier.padding(bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier), Modifier.padding(start = 3.dp, end = 1.dp, bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier),
enabled = attachmentEnabled enabled = attachmentEnabled
) { ) {
Icon( Icon(
@@ -81,10 +81,7 @@ private fun ContactPreferencesLayout(
reset: () -> Unit, reset: () -> Unit,
savePrefs: () -> Unit, savePrefs: () -> Unit,
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier
.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.contact_preferences)) AppBarTitle(stringResource(MR.strings.contact_preferences))
val timedMessages: MutableState<Boolean> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) } val timedMessages: MutableState<Boolean> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) }
val onTTLUpdated = { ttl: Int? -> val onTTLUpdated = { ttl: Int? ->
@@ -1,9 +1,11 @@
package chat.simplex.common.views.chat package chat.simplex.common.views.chat
import SectionBottomSpacer
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.QRCodeScanner import chat.simplex.common.views.newchat.QRCodeScanner
@@ -12,9 +14,7 @@ import dev.icerock.moko.resources.compose.stringResource
@Composable @Composable
fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) { fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
Column( ColumnWithScrollBar {
Modifier.fillMaxSize()
) {
AppBarTitle(stringResource(MR.strings.scan_code)) AppBarTitle(stringResource(MR.strings.scan_code))
QRCodeScanner { text -> QRCodeScanner { text ->
verifyCode(text) { verifyCode(text) {
@@ -28,5 +28,6 @@ fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: ()
} }
} }
Text(stringResource(MR.strings.scan_code_from_contacts_app), Modifier.padding(horizontal = DEFAULT_PADDING)) Text(stringResource(MR.strings.scan_code_from_contacts_app), Modifier.padding(horizontal = DEFAULT_PADDING))
SectionBottomSpacer()
} }
} }
@@ -12,6 +12,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.BackHandler import chat.simplex.common.platform.BackHandler
import chat.simplex.common.platform.chatModel import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
@@ -20,11 +21,12 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
@Composable @Composable
fun SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>) { fun BoxScope.SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>) {
val onBackClicked = { selectedChatItems.value = null } val onBackClicked = { selectedChatItems.value = null }
BackHandler(onBack = onBackClicked) BackHandler(onBack = onBackClicked)
val count = selectedChatItems.value?.size ?: 0 val count = selectedChatItems.value?.size ?: 0
DefaultTopAppBar( val oneHandUI = remember { appPrefs.oneHandUI.state }
DefaultAppBar(
navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) }, navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) },
title = { title = {
Text( Text(
@@ -39,10 +41,9 @@ fun SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>) {
) )
}, },
onTitleClick = null, onTitleClick = null,
showSearch = false, onTop = !oneHandUI.value,
onSearchValueChanged = {}, onSearchValueChanged = {},
) )
Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier))
} }
@Composable @Composable
@@ -68,6 +69,8 @@ fun SelectedItemsBottomToolbar(
Modifier Modifier
.matchParentSize() .matchParentSize()
.background(MaterialTheme.colors.background) .background(MaterialTheme.colors.background)
.padding(horizontal = 2.dp)
.height(AppBarHeight * fontSizeSqrtMultiplier)
.pointerInput(Unit) { .pointerInput(Unit) {
detectGesture { detectGesture {
true true
@@ -103,6 +106,7 @@ fun SelectedItemsBottomToolbar(
) )
} }
} }
Divider(Modifier.align(Alignment.TopStart))
} }
LaunchedEffect(chatInfo, chatItems, selectedChatItems.value) { LaunchedEffect(chatInfo, chatItems, selectedChatItems.value) {
recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited) recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited)
@@ -7,7 +7,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.* import androidx.compose.foundation.shape.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.* import androidx.compose.ui.*
@@ -15,6 +14,7 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
@@ -61,7 +61,8 @@ fun SendMsgView(
) { ) {
val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) } val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) }
Box(Modifier.padding(vertical = if (appPlatform.isAndroid) 8.dp else 6.dp)) { val padding = if (appPlatform.isAndroid) PaddingValues(vertical = 8.dp) else PaddingValues(top = 3.dp, bottom = 4.dp)
Box(Modifier.padding(padding)) {
val cs = composeState.value val cs = composeState.value
var progressByTimeout by rememberSaveable { mutableStateOf(false) } var progressByTimeout by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(composeState.value.inProgress) { LaunchedEffect(composeState.value.inProgress) {
@@ -147,7 +148,7 @@ fun SendMsgView(
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value) && (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
&& cs.contextItem is ComposeContextItem.NoContextItem && cs.contextItem is ComposeContextItem.NoContextItem
) { ) {
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(12.dp))
StartLiveMessageButton(userCanSend) { StartLiveMessageButton(userCanSend) {
if (composeState.value.preview is ComposePreview.NoPreview) { if (composeState.value.preview is ComposePreview.NoPreview) {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown) startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
@@ -423,6 +424,7 @@ private fun SendMsgButton(
onLongClick: (() -> Unit)? = null onLongClick: (() -> Unit)? = null
) { ) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val ripple = remember { ripple(bounded = false, radius = 24.dp) }
Box( Box(
modifier = Modifier.requiredSize(36.dp) modifier = Modifier.requiredSize(36.dp)
.combinedClickable( .combinedClickable(
@@ -431,7 +433,7 @@ private fun SendMsgButton(
enabled = enabled, enabled = enabled,
role = Role.Button, role = Role.Button,
interactionSource = interactionSource, interactionSource = interactionSource,
indication = rememberRipple(bounded = false, radius = 24.dp) indication = ripple
) )
.onRightClick { onLongClick?.invoke() }, .onRightClick { onLongClick?.invoke() },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -454,6 +456,7 @@ private fun SendMsgButton(
@Composable @Composable
private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) { private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val ripple = remember { ripple(bounded = false, radius = 24.dp) }
Box( Box(
modifier = Modifier.requiredSize(36.dp) modifier = Modifier.requiredSize(36.dp)
.clickable( .clickable(
@@ -461,7 +464,7 @@ private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
enabled = enabled, enabled = enabled,
role = Role.Button, role = Role.Button,
interactionSource = interactionSource, interactionSource = interactionSource,
indication = rememberRipple(bounded = false, radius = 24.dp) indication = ripple
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
@@ -56,11 +56,7 @@ private fun VerifyCodeLayout(
connectionVerified: Boolean, connectionVerified: Boolean,
verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit,
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) {
Modifier
.fillMaxSize()
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(MR.strings.security_code), withPadding = false) AppBarTitle(stringResource(MR.strings.security_code), withPadding = false)
val splitCode = splitToParts(connectionCode, 24) val splitCode = splitToParts(connectionCode, 24)
Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) { Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) {
@@ -130,10 +130,7 @@ fun AddGroupMembersLayout(
} }
} }
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier
.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.button_add_members)) AppBarTitle(stringResource(MR.strings.button_add_members))
profileText() profileText()
Spacer(Modifier.size(DEFAULT_PADDING)) Spacer(Modifier.size(DEFAULT_PADDING))
@@ -25,6 +25,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
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.withChats import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
@@ -283,9 +284,14 @@ fun ModalData.GroupChatInfoLayout(
if (s.isEmpty()) members else members.filter { m -> m.anyNameContains(s) } if (s.isEmpty()) members else members.filter { m -> m.anyNameContains(s) }
} }
} }
Box {
val oneHandUI = remember { appPrefs.oneHandUI.state }
LazyColumnWithScrollBar( LazyColumnWithScrollBar(
Modifier contentPadding = if (oneHandUI.value) {
.fillMaxWidth(), PaddingValues(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp, bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding())
} else {
PaddingValues(top = topPaddingToContent())
},
state = listState state = listState
) { ) {
item { item {
@@ -397,6 +403,11 @@ fun ModalData.GroupChatInfoLayout(
} }
} }
SectionBottomSpacer() SectionBottomSpacer()
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
}
}
if (!oneHandUI.value) {
NavigationBarBackground(oneHandUI.value, oneHandUI.value)
} }
} }
} }
@@ -119,9 +119,7 @@ fun GroupLinkLayout(
) )
} }
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier,
) {
AppBarTitle(stringResource(MR.strings.group_link)) AppBarTitle(stringResource(MR.strings.group_link))
Text( Text(
stringResource(MR.strings.you_can_share_group_link_anybody_will_be_able_to_connect), stringResource(MR.strings.you_can_share_group_link_anybody_will_be_able_to_connect),
@@ -313,10 +313,7 @@ fun GroupMemberInfoLayout(
} }
} }
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier
.fillMaxWidth(),
) {
Row( Row(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
@@ -82,9 +82,7 @@ private fun GroupPreferencesLayout(
reset: () -> Unit, reset: () -> Unit,
savePrefs: () -> Unit, savePrefs: () -> Unit,
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.group_preferences)) AppBarTitle(stringResource(MR.strings.group_preferences))
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) } val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) }
val onTTLUpdated = { ttl: Int? -> val onTTLUpdated = { ttl: Int? ->
@@ -82,10 +82,9 @@ fun GroupProfileLayout(
}, close) }, close)
} }
} }
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
ModalBottomSheetLayout( ModalBottomSheetLayout(
scrimColor = Color.Black.copy(alpha = 0.12F), scrimColor = Color.Black.copy(alpha = 0.12F),
modifier = Modifier.navigationBarsWithImePadding(), modifier = Modifier.imePadding(),
sheetContent = { sheetContent = {
GetImageBottomSheet( GetImageBottomSheet(
chosenImage, chosenImage,
@@ -98,9 +97,7 @@ fun GroupProfileLayout(
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
) { ) {
ModalView(close = closeWithAlert) { ModalView(close = closeWithAlert) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier
) {
Column( Column(
Modifier.fillMaxWidth() Modifier.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING) .padding(horizontal = DEFAULT_PADDING)
@@ -178,7 +175,6 @@ fun GroupProfileLayout(
} }
} }
} }
}
private fun canUpdateProfile(displayName: String, groupProfile: GroupProfile): Boolean = private fun canUpdateProfile(displayName: String, groupProfile: GroupProfile): Boolean =
displayName.trim().isNotEmpty() && isValidNewProfileName(displayName, groupProfile) displayName.trim().isNotEmpty() && isValidNewProfileName(displayName, groupProfile)
@@ -95,9 +95,7 @@ private fun GroupWelcomeLayout(
linkMode: SimplexLinkMode, linkMode: SimplexLinkMode,
save: () -> Unit, save: () -> Unit,
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxWidth(),
) {
val editMode = remember { mutableStateOf(true) } val editMode = remember { mutableStateOf(true) }
AppBarTitle(stringResource(MR.strings.group_welcome_title)) AppBarTitle(stringResource(MR.strings.group_welcome_title))
val wt = rememberSaveable { welcomeText } val wt = rememberSaveable { welcomeText }
@@ -10,6 +10,7 @@ import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.* import androidx.compose.ui.layout.*
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.platform.UriHandler
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
@@ -320,6 +321,8 @@ fun CIMarkdownText(
const val CHAT_IMAGE_LAYOUT_ID = "chatImage" const val CHAT_IMAGE_LAYOUT_ID = "chatImage"
const val CHAT_BUBBLE_LAYOUT_ID = "chatBubble" const val CHAT_BUBBLE_LAYOUT_ID = "chatBubble"
const val CHAT_COMPOSE_LAYOUT_ID = "chatCompose"
const val CONSOLE_COMPOSE_LAYOUT_ID = "consoleCompose"
/** /**
* Equal to [androidx.compose.ui.unit.Constraints.MaxFocusMask], which is 0x3FFFF - 1 * Equal to [androidx.compose.ui.unit.Constraints.MaxFocusMask], which is 0x3FFFF - 1
* Other values make a crash `java.lang.IllegalArgumentException: Can't represent a width of 123456 and height of 9909 in Constraints` * Other values make a crash `java.lang.IllegalArgumentException: Can't represent a width of 123456 and height of 9909 in Constraints`
@@ -398,6 +401,70 @@ fun DependentLayout(
} }
} }
} }
// The purpose of this layout is to make measuring of bottom compose view and adapt top lazy column to its size in the same frame (not on the next frame as you would expect).
// So, steps are:
// - measuring the layout: measured height of compose view before this step is 0, it's added to content padding of lazy column (so it's == 0)
// - measured the layout: measured height of compose view now is correct, but it's not yet applied to lazy column content padding (so it's == 0) and lazy column is placed higher than compose view in view with respect to compose view's height
// - on next frame measured height is correct and content padding is the same, lazy column placed to occupy all parent view's size
// - every added/removed line in compose view goes through the same process.
@Composable
fun AdaptingBottomPaddingLayout(
modifier: Modifier = Modifier,
mainLayoutId: String,
expectedHeight: MutableState<Dp>,
content: @Composable () -> Unit
) {
val expected = with(LocalDensity.current) { expectedHeight.value.roundToPx() }
Layout(
content = content,
modifier = modifier
) { measureable, constraints ->
require(measureable.size <= 2) { "Should be exactly one or two elements in this layout, you have ${measureable.size}" }
val mainPlaceable = measureable.firstOrNull { it.layoutId == mainLayoutId }!!.measure(constraints)
val placeables: List<Placeable> = measureable.map {
if (it.layoutId == mainLayoutId)
mainPlaceable
else
it.measure(constraints.copy(maxHeight = if (expected != mainPlaceable.measuredHeight) constraints.maxHeight - mainPlaceable.measuredHeight + expected else constraints.maxHeight)) }
expectedHeight.value = mainPlaceable.measuredHeight.toDp()
layout(constraints.maxWidth, constraints.maxHeight) {
var y = 0
placeables.forEach {
if (it !== mainPlaceable) {
it.place(0, y)
y += it.measuredHeight
} else {
it.place(0, constraints.maxHeight - mainPlaceable.measuredHeight)
y += it.measuredHeight
}
}
}
}
}
@Composable
fun CenteredRowLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measureable, constraints ->
require(measureable.size == 3) { "Should be exactly three elements in this layout, you have ${measureable.size}" }
val first = measureable[0].measure(constraints.copy(minWidth = 0, minHeight = 0))
val third = measureable[2].measure(constraints.copy(minWidth = first.measuredWidth, minHeight = 0))
val second = measureable[1].measure(constraints.copy(minWidth = 0, minHeight = 0, maxWidth = (constraints.maxWidth - first.measuredWidth - third.measuredWidth).coerceAtLeast(0)))
// Limit width for every other element to width of important element and height for a sum of all elements.
layout(constraints.maxWidth, constraints.maxHeight) {
first.place(0, ((constraints.maxHeight - first.measuredHeight) / 2).coerceAtLeast(0))
second.place((constraints.maxWidth - second.measuredWidth) / 2, ((constraints.maxHeight - second.measuredHeight) / 2).coerceAtLeast(0))
third.place(constraints.maxWidth - third.measuredWidth, ((constraints.maxHeight - third.measuredHeight) / 2).coerceAtLeast(0))
}
}
}
/* /*
class EditedProvider: PreviewParameterProvider<Boolean> { class EditedProvider: PreviewParameterProvider<Boolean> {
@@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.input.pointer.* import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.CryptoFile import chat.simplex.common.model.CryptoFile
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.ui.theme.CurrentColors
@@ -58,9 +57,17 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
val playersToRelease = rememberSaveable { mutableSetOf<URI>() } val playersToRelease = rememberSaveable { mutableSetOf<URI>() }
DisposableEffectOnGone( DisposableEffectOnGone(
always = { always = {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, Color.Black, false, false) platform.androidSetStatusAndNavigationBarAppearance(false, false, blackNavBar = true)
chatModel.fullscreenGalleryVisible.value = true
}, },
whenGone = { playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) } } whenDispose = {
val c = CurrentColors.value.colors
platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight)
chatModel.fullscreenGalleryVisible.value = false
},
whenGone = {
playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) }
}
) )
@Composable @Composable
@@ -10,8 +10,7 @@ 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.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.* import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
@@ -34,6 +33,7 @@ import chat.simplex.common.views.onboarding.shouldShowWhatsNew
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.views.call.Call import chat.simplex.common.views.call.Call
import chat.simplex.common.views.chat.item.CIFileViewScope import chat.simplex.common.views.chat.item.CIFileViewScope
import chat.simplex.common.views.chat.topPaddingToContent
import chat.simplex.common.views.newchat.* import chat.simplex.common.views.newchat.*
import chat.simplex.common.views.usersettings.* import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR import chat.simplex.res.MR
@@ -41,7 +41,6 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.net.URI
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
private fun showNewChatSheet(oneHandUI: State<Boolean>) { private fun showNewChatSheet(oneHandUI: State<Boolean>) {
@@ -55,7 +54,7 @@ private fun showNewChatSheet(oneHandUI: State<Boolean>) {
chatModel.newChatSheetVisible.value = false chatModel.newChatSheetVisible.value = false
close() close()
} }
ModalView(close, closeOnTop = !oneHandUI.value) { ModalView(close, showAppBar = !oneHandUI.value) {
if (appPlatform.isAndroid) { if (appPlatform.isAndroid) {
BackHandler { BackHandler {
close() close()
@@ -122,11 +121,7 @@ fun ToggleChatListCard() {
SharedPreferenceToggle( SharedPreferenceToggle(
appPrefs.oneHandUI, appPrefs.oneHandUI,
enabled = true, enabled = true
onChange = {
val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
}
) )
} }
} }
@@ -154,74 +149,36 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow<Animate
} }
} }
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
Scaffold( val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
topBar = { Box(Modifier.fillMaxSize()) {
if (!oneHandUI.value) {
Column {
ChatListToolbar(
userPickerState,
stopped,
setPerformLA,
)
Divider()
}
}
},
bottomBar = {
if (oneHandUI.value) { if (oneHandUI.value) {
Column { ChatListWithLoadingScreen(searchText, listState)
Divider() Column(Modifier.align(Alignment.BottomCenter)) {
ChatListToolbar( ChatListToolbar(
userPickerState, userPickerState,
listState,
stopped, stopped,
setPerformLA, setPerformLA,
) )
} }
} else {
ChatListWithLoadingScreen(searchText, listState)
Column {
ChatListToolbar(
userPickerState,
listState,
stopped,
setPerformLA,
)
} }
}, if (searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
contentColor = LocalContentColor.current, NewChatSheetFloatingButton(oneHandUI, stopped)
floatingActionButton = {
if (!oneHandUI.value && searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
FloatingActionButton(
onClick = {
if (!stopped) {
showNewChatSheet(oneHandUI)
}
},
Modifier
.padding(end = DEFAULT_PADDING - 16.dp, bottom = DEFAULT_PADDING - 16.dp)
.size(AppBarHeight * fontSizeSqrtMultiplier),
elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 0.dp,
pressedElevation = 0.dp,
hoveredElevation = 0.dp,
focusedElevation = 0.dp,
),
backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
contentColor = Color.White
) {
Icon(painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(22.dp * fontSizeSqrtMultiplier))
}
}
}
) {
Box(Modifier.padding(it)) {
Box(
modifier = Modifier
.fillMaxSize()
) {
if (!chatModel.desktopNoUserNoRemote) {
ChatList(chatModel, searchText = searchText)
}
if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
Text(stringResource(
if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary)
}
} }
} }
} }
if (searchText.value.text.isEmpty()) { if (searchText.value.text.isEmpty()) {
if (appPlatform.isDesktop) { if (appPlatform.isDesktop && !oneHandUI.value) {
val call = remember { chatModel.activeCall }.value val call = remember { chatModel.activeCall }.value
if (call != null) { if (call != null) {
ActiveCallInteractiveArea(call) ActiveCallInteractiveArea(call)
@@ -239,6 +196,46 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow<Animate
} }
} }
@Composable
private fun BoxScope.ChatListWithLoadingScreen(searchText: MutableState<TextFieldValue>, listState: LazyListState) {
if (!chatModel.desktopNoUserNoRemote) {
ChatList(searchText = searchText, listState)
}
if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
Text(
stringResource(
if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats
), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary
)
}
}
@Composable
private fun BoxScope.NewChatSheetFloatingButton(oneHandUI: State<Boolean>, stopped: Boolean) {
FloatingActionButton(
onClick = {
if (!stopped) {
showNewChatSheet(oneHandUI)
}
},
Modifier
.navigationBarsPadding()
.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING)
.align(Alignment.BottomEnd)
.size(AppBarHeight * fontSizeSqrtMultiplier),
elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 0.dp,
pressedElevation = 0.dp,
hoveredElevation = 0.dp,
focusedElevation = 0.dp,
),
backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
contentColor = Color.White
) {
Icon(painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(22.dp * fontSizeSqrtMultiplier))
}
}
@Composable @Composable
private fun ConnectButton(text: String, onClick: () -> Unit) { private fun ConnectButton(text: String, onClick: () -> Unit) {
Button( Button(
@@ -256,7 +253,7 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
} }
@Composable @Composable
private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, setPerformLA: (Boolean) -> Unit) { private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>, listState: LazyListState, stopped: Boolean, setPerformLA: (Boolean) -> Unit) {
val serversSummary: MutableState<PresentedServersSummary?> = remember { mutableStateOf(null) } val serversSummary: MutableState<PresentedServersSummary?> = remember { mutableStateOf(null) }
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
val updatingProgress = remember { chatModel.updatingProgress }.value val updatingProgress = remember { chatModel.updatingProgress }.value
@@ -265,6 +262,18 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
if (oneHandUI.value) { if (oneHandUI.value) {
val sp16 = with(LocalDensity.current) { 16.sp.toDp() } val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
if (appPlatform.isDesktop && oneHandUI.value) {
val call = remember { chatModel.activeCall }
if (call.value != null) {
barButtons.add {
val c = call.value
if (c != null) {
ActiveCallInteractiveArea(c)
Spacer(Modifier.width(5.dp))
}
}
}
}
if (!stopped) { if (!stopped) {
barButtons.add { barButtons.add {
IconButton( IconButton(
@@ -323,7 +332,9 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
} }
} }
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
DefaultTopAppBar( val scope = rememberCoroutineScope()
val canScrollToZero = remember { derivedStateOf { listState.firstVisibleItemIndex != 0 || listState.firstVisibleItemScrollOffset != 0 } }
DefaultAppBar(
navigationButton = { navigationButton = {
if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) { if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) {
NavigationButtonMenu { NavigationButtonMenu {
@@ -351,15 +362,14 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
SubscriptionStatusIndicator( SubscriptionStatusIndicator(
click = { click = {
ModalManager.start.closeModals() ModalManager.start.closeModals()
val summary = serversSummary.value
ModalManager.start.showModalCloseable( ModalManager.start.showModalCloseable(
endButtons = { endButtons = {
val summary = serversSummary.value
if (summary != null) { if (summary != null) {
ShareButton { ShareButton {
val json = Json { val json = Json {
prettyPrint = true prettyPrint = true
} }
val text = json.encodeToString(PresentedServersSummary.serializer(), summary) val text = json.encodeToString(PresentedServersSummary.serializer(), summary)
clipboard.shareText(text) clipboard.shareText(text)
} }
@@ -370,10 +380,10 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
) )
} }
}, },
onTitleClick = null, onTitleClick = if (canScrollToZero.value) { { scrollToBottom(scope, listState) } } else null,
showSearch = false, onTop = !oneHandUI.value,
onSearchValueChanged = {}, onSearchValueChanged = {},
buttons = barButtons buttons = { barButtons.forEach { it() } }
) )
} }
@@ -491,6 +501,7 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: String, chatModel: ChatModel) {
@Composable @Composable
private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>) { private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>) {
Box {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
var focused by remember { mutableStateOf(false) } var focused by remember { mutableStateOf(false) }
@@ -560,6 +571,9 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState
} }
} }
} }
val oneHandUI = remember { appPrefs.oneHandUI.state }
Divider(Modifier.align(if (oneHandUI.value) Alignment.TopStart else Alignment.BottomStart))
}
} }
private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<String?>, cleanup: (() -> Unit)?) { private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<String?>, cleanup: (() -> Unit)?) {
@@ -590,8 +604,37 @@ enum class ScrollDirection {
} }
@Composable @Composable
private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldValue>) { fun BoxScope.StatusBarBackground() {
val listState = rememberLazyListState(lazyListState.first, lazyListState.second) if (appPlatform.isAndroid) {
val finalColor = MaterialTheme.colors.background.copy(0.88f)
Box(Modifier.fillMaxWidth().windowInsetsTopHeight(WindowInsets.statusBars).background(finalColor))
}
}
@Composable
fun BoxScope.NavigationBarBackground(appBarOnBottom: Boolean = false, mixedColor: Boolean, noAlpha: Boolean = false) {
if (appPlatform.isAndroid) {
val barPadding = WindowInsets.navigationBars.asPaddingValues()
val paddingBottom = barPadding.calculateBottomPadding()
val color = if (mixedColor) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) else MaterialTheme.colors.background
val finalColor = color.copy(if (noAlpha) 1f else if (appBarOnBottom) remember { appPrefs.inAppBarsAlpha.state }.value else 0.6f)
Box(Modifier.align(Alignment.BottomStart).height(paddingBottom).fillMaxWidth().background(finalColor))
}
}
@Composable
fun BoxScope.NavigationBarBackground(modifier: Modifier, color: Color = MaterialTheme.colors.background) {
val keyboardState = getKeyboardState()
if (appPlatform.isAndroid && keyboardState.value == KeyboardState.Closed) {
val barPadding = WindowInsets.navigationBars.asPaddingValues()
val paddingBottom = barPadding.calculateBottomPadding()
val finalColor = color.copy(0.6f)
Box(modifier.align(Alignment.BottomStart).height(paddingBottom).fillMaxWidth().background(finalColor))
}
}
@Composable
private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listState: LazyListState) {
var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) } var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) }
var previousIndex by remember { mutableStateOf(0) } var previousIndex by remember { mutableStateOf(0) }
var previousScrollOffset by remember { mutableStateOf(0) } var previousScrollOffset by remember { mutableStateOf(0) }
@@ -628,40 +671,45 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
val searchShowingSimplexLink = remember { mutableStateOf(false) } val searchShowingSimplexLink = remember { mutableStateOf(false) }
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) } val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList()) val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList())
val topPaddingToContent = topPaddingToContent()
val blankSpaceSize = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else topPaddingToContent
LazyColumnWithScrollBar( LazyColumnWithScrollBar(
Modifier.fillMaxSize(), if (!oneHandUI.value) Modifier.imePadding() else Modifier,
listState, listState,
reverseLayout = oneHandUI.value reverseLayout = oneHandUI.value
) { ) {
item { Spacer(Modifier.height(blankSpaceSize)) }
stickyHeader { stickyHeader {
Column( Column(
Modifier Modifier
.zIndex(1f)
.offset { .offset {
val y = if (searchText.value.text.isEmpty()) {
val offsetMultiplier = if (oneHandUI.value) 1 else -1 val offsetMultiplier = if (oneHandUI.value) 1 else -1
if ( val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened) || scrollDirection == ScrollDirection.Up) {
(oneHandUI.value && scrollDirection == ScrollDirection.Up) || if (listState.firstVisibleItemIndex == 0) -offsetMultiplier * listState.firstVisibleItemScrollOffset
(appPlatform.isAndroid && keyboardState == KeyboardState.Opened) else -offsetMultiplier * blankSpaceSize.roundToPx()
) {
0
} else if (listState.firstVisibleItemIndex == 0) offsetMultiplier * listState.firstVisibleItemScrollOffset else offsetMultiplier * 1000
} else { } else {
0 when (listState.firstVisibleItemIndex) {
0 -> 0
1 -> offsetMultiplier * listState.firstVisibleItemScrollOffset
else -> offsetMultiplier * 1000
}
} }
IntOffset(0, y) IntOffset(0, y)
} }
.background(MaterialTheme.colors.background), .background(MaterialTheme.colors.background)
) { ) {
if (oneHandUI.value) { if (oneHandUI.value) {
Divider() Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) {
} ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
}
} else {
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
if (!oneHandUI.value) {
Divider()
} }
} }
} }
if (appPlatform.isAndroid && !oneHandUICardShown.value && chats.count() > 1) { if (!oneHandUICardShown.value && chats.size > 1) {
item { item {
ToggleChatListCard() ToggleChatListCard()
} }
@@ -672,17 +720,30 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
} } } }
ChatListNavLinkView(chat, nextChatSelected) ChatListNavLinkView(chat, nextChatSelected)
} }
if (appPlatform.isAndroid && !oneHandUICardShown.value && chats.count() <= 1) { if (!oneHandUICardShown.value && chats.size <= 1) {
item { item {
ToggleChatListCard() ToggleChatListCard()
} }
} }
if (appPlatform.isAndroid) {
item { Spacer(if (oneHandUI.value) Modifier.windowInsetsTopHeight(WindowInsets.statusBars) else Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) }
}
} }
if (chats.isEmpty() && chatModel.chats.value.isNotEmpty()) { if (chats.isEmpty() && chatModel.chats.value.isNotEmpty()) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(Modifier.fillMaxSize().imePadding(), contentAlignment = Alignment.Center) {
Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary) Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary)
} }
} }
if (oneHandUI.value) {
StatusBarBackground()
} else {
NavigationBarBackground(oneHandUI.value, true)
}
if (!oneHandUICardShown.value) {
LaunchedEffect(chats.size) {
if (chats.size >= 3) appPrefs.oneHandUICardShown.set(true)
}
}
} }
fun filteredChats( fun filteredChats(
@@ -727,3 +788,7 @@ private fun filtered(chat: Chat): Boolean =
(chat.chatInfo.chatSettings?.favorite ?: false) || (chat.chatInfo.chatSettings?.favorite ?: false) ||
chat.chatStats.unreadChat || chat.chatStats.unreadChat ||
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0) (chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
fun scrollToBottom(scope: CoroutineScope, listState: LazyListState) {
scope.launch { try { listState.animateScrollToItem(0) } catch (e: Exception) { Log.e(TAG, e.stackTraceToString()) } }
}
@@ -620,9 +620,7 @@ fun ModalData.SMPServerSummaryView(
ModalView( ModalView(
close = close close = close
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxSize(),
) {
val bottomPadding = DEFAULT_PADDING val bottomPadding = DEFAULT_PADDING
AppBarTitle( AppBarTitle(
stringResource(MR.strings.smp_server), stringResource(MR.strings.smp_server),
@@ -645,9 +643,7 @@ fun ModalData.DetailedXFTPStatsView(
ModalView( ModalView(
close = close close = close
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxSize(),
) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
val bottomPadding = DEFAULT_PADDING val bottomPadding = DEFAULT_PADDING
AppBarTitle( AppBarTitle(
@@ -671,9 +667,7 @@ fun ModalData.DetailedSMPStatsView(
ModalView( ModalView(
close = close close = close
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxSize(),
) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
val bottomPadding = DEFAULT_PADDING val bottomPadding = DEFAULT_PADDING
AppBarTitle( AppBarTitle(
@@ -697,9 +691,7 @@ fun ModalData.XFTPServerSummaryView(
ModalView( ModalView(
close = close close = close
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxSize(),
) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
val bottomPadding = DEFAULT_PADDING val bottomPadding = DEFAULT_PADDING
AppBarTitle( AppBarTitle(
@@ -715,9 +707,7 @@ fun ModalData.XFTPServerSummaryView(
@Composable @Composable
fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableState<PresentedServersSummary?>) { fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableState<PresentedServersSummary?>) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxSize(),
) {
var showUserSelection by remember { mutableStateOf(false) } var showUserSelection by remember { mutableStateOf(false) }
val selectedUserCategory = val selectedUserCategory =
remember { stateGetOrPut("selectedUserCategory") { PresentedUserCategory.ALL_USERS } } remember { stateGetOrPut("selectedUserCategory") { PresentedUserCategory.ALL_USERS } }
@@ -11,10 +11,13 @@ import androidx.compose.ui.graphics.Color
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.unit.dp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.themedBackground
import chat.simplex.common.views.chat.topPaddingToContent
import chat.simplex.common.views.newchat.ActiveProfilePicker import chat.simplex.common.views.newchat.ActiveProfilePicker
import chat.simplex.res.MR import chat.simplex.res.MR
@@ -22,26 +25,7 @@ import chat.simplex.res.MR
fun ShareListView(chatModel: ChatModel, stopped: Boolean) { fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
var searchInList by rememberSaveable { mutableStateOf("") } var searchInList by rememberSaveable { mutableStateOf("") }
val oneHandUI = remember { appPrefs.oneHandUI.state } val oneHandUI = remember { appPrefs.oneHandUI.state }
Box(Modifier.fillMaxSize().themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) {
Scaffold(
contentColor = LocalContentColor.current,
topBar = {
if (!oneHandUI.value) {
Column {
ShareListToolbar(chatModel, stopped) { searchInList = it.trim() }
Divider()
}
}
},
bottomBar = {
if (oneHandUI.value) {
Column {
Divider()
ShareListToolbar(chatModel, stopped) { searchInList = it.trim() }
}
}
}
) {
val sharedContent = chatModel.sharedContent.value val sharedContent = chatModel.sharedContent.value
var isMediaOrFileAttachment = false var isMediaOrFileAttachment = false
var isVoice = false var isVoice = false
@@ -69,10 +53,6 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
} }
null -> {} null -> {}
} }
Box(Modifier.padding(it)) {
Column(
modifier = Modifier.fillMaxSize()
) {
if (chatModel.chats.value.isNotEmpty()) { if (chatModel.chats.value.isNotEmpty()) {
ShareList( ShareList(
chatModel, chatModel,
@@ -84,7 +64,13 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
} else { } else {
EmptyList() EmptyList()
} }
if (oneHandUI.value) {
StatusBarBackground()
} else {
NavigationBarBackground(oneHandUI.value, true)
} }
Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) {
ShareListToolbar(chatModel, stopped) { searchInList = it.trim() }
} }
} }
} }
@@ -108,7 +94,6 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
if (showSearch) { if (showSearch) {
BackHandler(onBack = hideSearchOnBack) BackHandler(onBack = hideSearchOnBack)
} }
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } } val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
val navButton: @Composable RowScope.() -> Unit = { val navButton: @Composable RowScope.() -> Unit = {
when { when {
@@ -118,13 +103,13 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
.filter { u -> !u.user.activeUser && !u.user.hidden } .filter { u -> !u.user.activeUser && !u.user.hidden }
.all { u -> u.unreadCount == 0 } .all { u -> u.unreadCount == 0 }
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) { UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
ModalManager.start.showCustomModal { close -> ModalManager.start.showCustomModal(keyboardCoversBar = false) { close ->
val search = rememberSaveable { mutableStateOf("") } val search = rememberSaveable { mutableStateOf("") }
ModalView( ModalView(
{ close() }, { close() },
endButtons = { showSearch = true,
SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } searchAlwaysVisible = true,
}, onSearchValueChanged = { search.value = it },
content = { content = {
ActiveProfilePicker( ActiveProfilePicker(
search = search, search = search,
@@ -148,31 +133,8 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
}) })
} }
} }
if (chatModel.chats.value.size >= 8) {
barButtons.add {
IconButton({ showSearch = true }) {
Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary)
}
}
}
if (stopped) {
barButtons.add {
IconButton(onClick = {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.chat_is_stopped_indication),
generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app)
)
}) {
Icon(
painterResource(MR.images.ic_report_filled),
generalGetString(MR.strings.chat_is_stopped_indication),
tint = Color.Red,
)
}
}
}
DefaultTopAppBar( DefaultAppBar(
navigationButton = navButton, navigationButton = navButton,
title = { title = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@@ -191,8 +153,29 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
}, },
onTitleClick = null, onTitleClick = null,
showSearch = showSearch, showSearch = showSearch,
onTop = !remember { appPrefs.oneHandUI.state }.value,
onSearchValueChanged = onSearchValueChanged, onSearchValueChanged = onSearchValueChanged,
buttons = barButtons buttons = {
if (chatModel.chats.value.size >= 8) {
IconButton({ showSearch = true }) {
Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary)
}
}
if (stopped) {
IconButton(onClick = {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.chat_is_stopped_indication),
generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app)
)
}) {
Icon(
painterResource(MR.images.ic_report_filled),
generalGetString(MR.strings.chat_is_stopped_indication),
tint = Color.Red,
)
}
}
}
) )
} }
@@ -211,8 +194,13 @@ private fun ShareList(
filteredChats(false, mutableStateOf(false), mutableStateOf(null), search, sorted) filteredChats(false, mutableStateOf(false), mutableStateOf(null), search, sorted)
} }
} }
val topPaddingToContent = topPaddingToContent()
LazyColumnWithScrollBar( LazyColumnWithScrollBar(
modifier = Modifier.fillMaxSize(), modifier = Modifier.then(if (oneHandUI.value) Modifier.consumeWindowInsets(WindowInsets.navigationBars.only(WindowInsetsSides.Vertical)) else Modifier).imePadding(),
contentPadding = PaddingValues(
top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else topPaddingToContent,
bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp
),
reverseLayout = oneHandUI.value reverseLayout = oneHandUI.value
) { ) {
items(chats) { chat -> items(chats) { chat ->
@@ -23,6 +23,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
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.ChatController.stopRemoteHostAndReloadHosts import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts
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.*
@@ -137,12 +138,16 @@ fun UserPicker(
} }
} }
val oneHandUI = remember { appPrefs.oneHandUI.state }
val iconColor = MaterialTheme.colors.secondaryVariant
val background = if (appPlatform.isAndroid) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha()) else MaterialTheme.colors.surface
PlatformUserPicker( PlatformUserPicker(
modifier = Modifier modifier = Modifier
.height(IntrinsicSize.Min) .height(IntrinsicSize.Min)
.fillMaxWidth() .fillMaxWidth()
.then(if (newChat.isVisible()) Modifier.shadow(8.dp, clip = true) else Modifier) .then(if (newChat.isVisible()) Modifier.shadow(8.dp, clip = true, ambientColor = background) else Modifier)
.background(if (appPlatform.isAndroid) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha()) else MaterialTheme.colors.surface) .padding(top = if (appPlatform.isDesktop && oneHandUI.value) 7.dp else 0.dp)
.background(background)
.padding(bottom = USER_PICKER_SECTION_SPACING - DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL), .padding(bottom = USER_PICKER_SECTION_SPACING - DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL),
pickerState = userPickerState pickerState = userPickerState
) { ) {
@@ -198,12 +203,13 @@ fun UserPicker(
UserPickerUsersSection( UserPickerUsersSection(
users = users, users = users,
onUserClicked = onUserClicked, onUserClicked = onUserClicked,
iconColor = iconColor,
stopped = stopped stopped = stopped
) )
} }
} else if (currentUser != null) { } else if (currentUser != null) {
SectionItemView({ onUserClicked(currentUser) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) { SectionItemView({ onUserClicked(currentUser) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) {
ProfilePreview(currentUser.profile, stopped = stopped) ProfilePreview(currentUser.profile, iconColor = iconColor, stopped = stopped)
} }
} }
} }
@@ -234,6 +240,7 @@ fun UserPicker(
Column(modifier = Modifier.padding(vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL)) { Column(modifier = Modifier.padding(vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL)) {
UserPickerUsersSection( UserPickerUsersSection(
users = inactiveUsers, users = inactiveUsers,
iconColor = iconColor,
onUserClicked = onUserClicked, onUserClicked = onUserClicked,
stopped = stopped stopped = stopped
) )
@@ -265,13 +272,15 @@ fun UserPicker(
generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_open_chat_profiles),
generalGetString(MR.strings.auth_log_in_using_credential) generalGetString(MR.strings.auth_log_in_using_credential)
) { ) {
ModalManager.start.showCustomModal { close -> ModalManager.start.showCustomModal(keyboardCoversBar = false) { close ->
val search = rememberSaveable { mutableStateOf("") } val search = rememberSaveable { mutableStateOf("") }
val profileHidden = rememberSaveable { mutableStateOf(false) } val profileHidden = rememberSaveable { mutableStateOf(false) }
ModalView( ModalView(
{ close() }, { close() },
endButtons = { showSearch = true,
SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } searchAlwaysVisible = true,
onSearchValueChanged = {
search.value = it
}, },
content = { UserProfilesView(chatModel, search, profileHidden) }) content = { UserProfilesView(chatModel, search, profileHidden) })
} }
@@ -519,6 +528,7 @@ private fun DevicePickerRow(
@Composable @Composable
expect fun UserPickerUsersSection( expect fun UserPickerUsersSection(
users: List<UserInfo>, users: List<UserInfo>,
iconColor: Color,
stopped: Boolean, stopped: Boolean,
onUserClicked: (user: User) -> Unit, onUserClicked: (user: User) -> Unit,
) )
@@ -46,9 +46,7 @@ fun ChatArchiveLayout(
saveArchive: () -> Unit, saveArchive: () -> Unit,
deleteArchiveAlert: () -> Unit deleteArchiveAlert: () -> Unit
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxWidth(),
) {
AppBarTitle(title) AppBarTitle(title)
SectionView(stringResource(MR.strings.chat_archive_section)) { SectionView(stringResource(MR.strings.chat_archive_section)) {
SettingsActionItem( SettingsActionItem(
@@ -203,7 +203,7 @@ fun DatabaseEncryptionLayout(
Layout() Layout()
} }
} else { } else {
ColumnWithScrollBar(Modifier.fillMaxWidth(), maxIntrinsicSize = true) { ColumnWithScrollBar(maxIntrinsicSize = true) {
Layout() Layout()
} }
} }
@@ -77,10 +77,7 @@ fun DatabaseErrorView(
Text(String.format(generalGetString(MR.strings.database_migrations), ms.joinToString(", "))) Text(String.format(generalGetString(MR.strings.database_migrations), ms.joinToString(", ")))
} }
ColumnWithScrollBar( ColumnWithScrollBarNoAppBar(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
) {
val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value
when (val status = chatDbStatus.value) { when (val status = chatDbStatus.value) {
is DBMigrationResult.ErrorNotADatabase -> is DBMigrationResult.ErrorNotADatabase ->
@@ -156,9 +156,7 @@ fun DatabaseLayout(
val stopped = !runChat val stopped = !runChat
val operationsDisabled = (!stopped || progressIndicator) && !chatModel.desktopNoUserNoRemote val operationsDisabled = (!stopped || progressIndicator) && !chatModel.desktopNoUserNoRemote
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.your_chat_database)) AppBarTitle(stringResource(MR.strings.your_chat_database))
if (!chatModel.desktopNoUserNoRemote) { if (!chatModel.desktopNoUserNoRemote) {
@@ -0,0 +1,71 @@
package chat.simplex.common.views.helpers
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.graphics.*
import androidx.compose.ui.unit.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chatlist.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import kotlin.math.absoluteValue
@Composable
fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f + 8.dp) {
val handler = LocalAppBarHandler.current
val connection = handler?.connection
LaunchedEffect(title) {
handler?.title?.value = title
}
val theme = CurrentColors.collectAsState()
val titleColor = MaterialTheme.appColors.title
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))
else // color is not updated when changing themes if I pass null here
Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
Column {
Text(
title,
Modifier
.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, top = DEFAULT_PADDING_HALF, end = if (withPadding) DEFAULT_PADDING else 0.dp,)
.graphicsLayer {
alpha = bottomTitleAlpha(connection)
},
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h1.copy(brush = brush),
color = MaterialTheme.colors.primaryVariant,
textAlign = TextAlign.Start
)
if (hostDevice != null) {
Box(Modifier.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp).graphicsLayer {
alpha = bottomTitleAlpha(connection)
}) {
HostDeviceTitle(hostDevice)
}
}
Spacer(Modifier.height(bottomPadding))
}
}
private fun bottomTitleAlpha(connection: CollapsingAppBarNestedScrollConnection?) =
if ((connection?.appBarOffset ?: 0f).absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 1f
else ((AppBarHandler.appBarMaxHeightPx) + (connection?.appBarOffset ?: 0f) / 1.5f).coerceAtLeast(0f) / AppBarHandler.appBarMaxHeightPx
@Composable
private fun HostDeviceTitle(hostDevice: Pair<Long?, String>, extraPadding: Boolean = false) {
Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) {
DevicePill(
active = true,
onClick = {},
actionButtonVisible = false,
icon = painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300),
text = hostDevice.second
)
}
}
@@ -0,0 +1,139 @@
package chat.simplex.common.views.helpers
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.unit.*
import chat.simplex.common.platform.appPlatform
import chat.simplex.common.ui.theme.CurrentColors
fun Modifier.blurredBackgroundModifier(
keyboardInset: WindowInsets,
handler: AppBarHandler?,
blurRadius: State<Int>,
prefAlpha: State<Float>,
keyboardCoversBar: Boolean,
onTop: Boolean,
density: Density
): Modifier {
val graphicsLayer = handler?.graphicsLayer
val backgroundGraphicsLayer = handler?.backgroundGraphicsLayer
val backgroundGraphicsLayerSize = handler?.backgroundGraphicsLayerSize
if (handler == null || graphicsLayer == null || backgroundGraphicsLayer == null || blurRadius.value == 0 || prefAlpha.value == 1f || backgroundGraphicsLayerSize === null)
return this
return if (appPlatform.isAndroid) {
this.androidBlurredModifier(keyboardInset, blurRadius.value, keyboardCoversBar, onTop, graphicsLayer, backgroundGraphicsLayer, backgroundGraphicsLayerSize, density)
} else {
this.desktopBlurredModifier(keyboardInset, blurRadius, keyboardCoversBar, onTop, graphicsLayer, backgroundGraphicsLayer, backgroundGraphicsLayerSize, density)
}
}
// this is more performant version than for Android but can't be used on desktop because on first frame it shows transparent view
// which is very noticeable on desktop and unnoticeable on Android
private fun Modifier.androidBlurredModifier(
keyboardInset: WindowInsets,
blurRadius: Int,
keyboardCoversBar: Boolean,
onTop: Boolean,
graphicsLayer: GraphicsLayer,
backgroundGraphicsLayer: GraphicsLayer,
backgroundGraphicsLayerSize: State<IntSize>,
density: Density
): Modifier = this
.graphicsLayer {
renderEffect = if (blurRadius > 0) BlurEffect(blurRadius.dp.toPx(), blurRadius.dp.toPx()) else null
clip = blurRadius > 0
}
.graphicsLayer {
if (!onTop) {
val bgSize = when {
backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height
backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height
else -> backgroundGraphicsLayerSize.value.height
}
val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0
translationY = -bgSize + size.height + keyboardHeightCovered
}
}
.drawBehind {
drawRect(Color.Black)
if (onTop) {
clipRect {
if (backgroundGraphicsLayer.size != IntSize.Zero) {
drawLayer(backgroundGraphicsLayer)
} else {
drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat()))
}
drawLayer(graphicsLayer)
}
} else {
if (backgroundGraphicsLayer.size != IntSize.Zero) {
drawLayer(backgroundGraphicsLayer)
} else {
drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat()))
}
drawLayer(graphicsLayer)
}
}
.graphicsLayer {
if (!onTop) {
val bgSize = when {
backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height
backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height
else -> backgroundGraphicsLayerSize.value.height
}
val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0
translationY -= -bgSize + size.height + keyboardHeightCovered
}
}
private fun Modifier.desktopBlurredModifier(
keyboardInset: WindowInsets,
blurRadius: State<Int>,
keyboardCoversBar: Boolean,
onTop: Boolean,
graphicsLayer: GraphicsLayer,
backgroundGraphicsLayer: GraphicsLayer,
backgroundGraphicsLayerSize: State<IntSize>,
density: Density
): Modifier = this
.graphicsLayer {
renderEffect = if (blurRadius.value > 0) BlurEffect(blurRadius.value.dp.toPx(), blurRadius.value.dp.toPx()) else null
clip = blurRadius.value > 0
}
.drawBehind {
drawRect(Color.Black)
if (onTop) {
clipRect {
if (backgroundGraphicsLayer.size != IntSize.Zero) {
drawLayer(backgroundGraphicsLayer)
} else {
drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat()))
}
drawLayer(graphicsLayer)
}
} else {
val bgSize = when {
backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height
backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height
else -> backgroundGraphicsLayerSize.value.height
}
val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0
translate(top = -bgSize + size.height + keyboardHeightCovered) {
if (backgroundGraphicsLayer.size != IntSize.Zero) {
drawLayer(backgroundGraphicsLayer)
} else {
drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat()))
}
drawLayer(graphicsLayer)
}
}
}
@@ -1,11 +1,13 @@
package chat.simplex.common.views.helpers package chat.simplex.common.views.helpers
import androidx.compose.runtime.*
import androidx.compose.ui.draw.CacheDrawScope import androidx.compose.ui.draw.CacheDrawScope
import androidx.compose.ui.draw.DrawResult import androidx.compose.ui.draw.DrawResult
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.* import androidx.compose.ui.graphics.drawscope.*
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.appPrefs
@@ -381,7 +383,14 @@ private fun drawToBitmap(image: ImageBitmap, imageScale: Float, tint: Color, siz
return bitmap return bitmap
} }
fun CacheDrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperType, background: Color, tint: Color): DrawResult { fun CacheDrawScope.chatViewBackground(
image: ImageBitmap,
imageType: WallpaperType,
background: Color,
tint: Color,
graphicsLayerSize: MutableState<IntSize>? = null,
backgroundGraphicsLayer: GraphicsLayer? = null
): DrawResult {
val imageScale = if (imageType is WallpaperType.Preset) { val imageScale = if (imageType is WallpaperType.Preset) {
(imageType.scale ?: 1f) * imageType.predefinedImageScale (imageType.scale ?: 1f) * imageType.predefinedImageScale
} else if (imageType is WallpaperType.Image && imageType.scaleType == WallpaperScaleType.REPEAT) { } else if (imageType is WallpaperType.Image && imageType.scaleType == WallpaperScaleType.REPEAT) {
@@ -396,6 +405,7 @@ fun CacheDrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperTy
} }
return onDrawBehind { return onDrawBehind {
copyBackgroundToAppBar(graphicsLayerSize, backgroundGraphicsLayer) {
val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low
drawRect(background) drawRect(background)
when (imageType) { when (imageType) {
@@ -446,3 +456,4 @@ fun CacheDrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperTy
} }
} }
} }
}
@@ -19,6 +19,8 @@ fun ChooseAttachmentView(attachmentOption: MutableState<AttachmentOption?>, hide
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.navigationBarsPadding()
.imePadding()
.wrapContentHeight() .wrapContentHeight()
.onFocusChanged { focusState -> .onFocusChanged { focusState ->
if (!focusState.hasFocus) hide() if (!focusState.hasFocus) hide()
@@ -1,181 +0,0 @@
package chat.simplex.common.views.helpers
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.background
import androidx.compose.ui.draw.*
import androidx.compose.ui.graphics.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.*
import chat.simplex.common.platform.appPlatform
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chatlist.DevicePill
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import kotlin.math.absoluteValue
@Composable
fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Color = if (close != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, arrangement: Arrangement.Vertical = Arrangement.Top, closeBarTitle: String? = null, barPaddingValues: PaddingValues = PaddingValues(horizontal = AppBarHorizontalPadding), endButtons: @Composable RowScope.() -> Unit = {}) {
var rowModifier = Modifier
.fillMaxWidth()
.height(AppBarHeight * fontSizeSqrtMultiplier)
val themeBackgroundMix = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f)
if (!closeBarTitle.isNullOrEmpty()) {
rowModifier = rowModifier.background(themeBackgroundMix)
}
val handler = LocalAppBarHandler.current
val connection = LocalAppBarHandler.current?.connection
val title = remember(handler?.title?.value) { handler?.title ?: mutableStateOf("") }
Column(
verticalArrangement = arrangement,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = AppBarHeight * fontSizeSqrtMultiplier)
.drawWithCache {
val backgroundColor = if (appPlatform.isDesktop && connection != null) themeBackgroundMix.copy(alpha = topTitleAlpha(connection)) else Color.Transparent
onDrawBehind {
if (appPlatform.isDesktop) {
drawRect(backgroundColor)
}
}
}
) {
Row(
modifier = Modifier.padding(barPaddingValues),
content = {
Row(
rowModifier,
verticalAlignment = Alignment.CenterVertically
) {
if (showClose) {
NavigationButtonBack(tintColor = tintColor, onButtonClicked = close)
} else {
Spacer(Modifier)
}
if (!closeBarTitle.isNullOrEmpty()) {
Row(
Modifier.weight(1f),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(
closeBarTitle,
fontWeight = FontWeight.SemiBold,
maxLines = 1
)
}
} else if (title.value.isNotEmpty() && connection != null) {
Row(
Modifier
.padding(start = if (showClose) 0.dp else DEFAULT_PADDING_HALF)
.weight(1f) // hides the title if something wants full width (eg, search field in chat profiles screen)
.graphicsLayer {
alpha = topTitleAlpha((connection))
}
.padding(start = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
title.value,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
} else {
Spacer(Modifier.weight(1f))
}
Row {
endButtons()
}
}
}
)
if (closeBarTitle.isNullOrEmpty() && title.value.isNotEmpty() && connection != null) {
Divider(
Modifier
.graphicsLayer {
alpha = topTitleAlpha(connection)
}
)
}
}
}
@Composable
fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f + 8.dp) {
val handler = LocalAppBarHandler.current
val connection = handler?.connection
LaunchedEffect(title) {
handler?.title?.value = title
}
val theme = CurrentColors.collectAsState()
val titleColor = MaterialTheme.appColors.title
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))
else // color is not updated when changing themes if I pass null here
Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
Column {
Text(
title,
Modifier
.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, top = DEFAULT_PADDING_HALF, end = if (withPadding) DEFAULT_PADDING else 0.dp,)
.graphicsLayer {
alpha = bottomTitleAlpha(connection)
},
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h1.copy(brush = brush),
color = MaterialTheme.colors.primaryVariant,
textAlign = TextAlign.Start
)
if (hostDevice != null) {
Box(Modifier.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp).graphicsLayer {
alpha = bottomTitleAlpha(connection)
}) {
HostDeviceTitle(hostDevice)
}
}
Spacer(Modifier.height(bottomPadding))
}
}
private fun topTitleAlpha(connection: CollapsingAppBarNestedScrollConnection) =
if (connection.appBarOffset.absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 0f
else ((-connection.appBarOffset * 1.5f) / (AppBarHandler.appBarMaxHeightPx)).coerceIn(0f, 1f)
private fun bottomTitleAlpha(connection: CollapsingAppBarNestedScrollConnection?) =
if ((connection?.appBarOffset ?: 0f).absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 1f
else ((AppBarHandler.appBarMaxHeightPx) + (connection?.appBarOffset ?: 0f) / 1.5f).coerceAtLeast(0f) / AppBarHandler.appBarMaxHeightPx
@Composable
private fun HostDeviceTitle(hostDevice: Pair<Long?, String>, extraPadding: Boolean = false) {
Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) {
DevicePill(
active = true,
onClick = {},
actionButtonVisible = false,
icon = painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300),
text = hostDevice.second
)
}
}
@Preview/*(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true,
name = "Dark Mode"
)*/
@Composable
fun PreviewCloseSheetBar() {
SimpleXTheme {
CloseSheetBar(close = {})
}
}
@@ -3,15 +3,67 @@ package chat.simplex.common.views.helpers
import androidx.compose.foundation.ScrollState import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.*
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.graphics.rememberGraphicsLayer
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.IntSize
import chat.simplex.common.model.ChatController.appPrefs
val LocalAppBarHandler: ProvidableCompositionLocal<AppBarHandler?> = staticCompositionLocalOf { null } val LocalAppBarHandler: ProvidableCompositionLocal<AppBarHandler?> = staticCompositionLocalOf { null }
@Composable
fun rememberAppBarHandler(key1: Any? = null, key2: Any? = null, keyboardCoversBar: Boolean = true): AppBarHandler {
val graphicsLayer = rememberGraphicsLayer()
val backgroundGraphicsLayer = rememberGraphicsLayer()
return remember(key1, key2) { AppBarHandler(graphicsLayer, backgroundGraphicsLayer, keyboardCoversBar) }
}
@Composable
fun adjustAppBarHandler(handler: AppBarHandler): AppBarHandler {
val graphicsLayer = rememberGraphicsLayer()
val backgroundGraphicsLayer = rememberGraphicsLayer()
if (handler.graphicsLayer == null || handler.graphicsLayer?.isReleased == true || handler.backgroundGraphicsLayer?.isReleased == true) {
handler.graphicsLayer = graphicsLayer
handler.backgroundGraphicsLayer = backgroundGraphicsLayer
}
return handler
}
fun Modifier.copyViewToAppBar(blurRadius: Int, graphicsLayer: GraphicsLayer?): Modifier {
return if (blurRadius > 0 && graphicsLayer != null) {
this.drawWithContent {
graphicsLayer.record {
this@drawWithContent.drawContent()
}
drawLayer(graphicsLayer)
}
} else this
}
fun DrawScope.copyBackgroundToAppBar(graphicsLayerSize: MutableState<IntSize>?, backgroundGraphicsLayer: GraphicsLayer?, scope: DrawScope.() -> Unit) {
val blurRadius = appPrefs.appearanceBarsBlurRadius.get()
if (blurRadius > 0 && graphicsLayerSize != null && backgroundGraphicsLayer != null) {
graphicsLayerSize.value = backgroundGraphicsLayer.size
backgroundGraphicsLayer.record {
scope()
}
drawLayer(backgroundGraphicsLayer)
} else {
scope()
}
}
@Stable @Stable
class AppBarHandler( class AppBarHandler(
var graphicsLayer: GraphicsLayer?,
var backgroundGraphicsLayer: GraphicsLayer?,
val keyboardCoversBar: Boolean = true,
listState: LazyListState = LazyListState(0, 0), listState: LazyListState = LazyListState(0, 0),
scrollState: ScrollState = ScrollState(initial = 0) scrollState: ScrollState = ScrollState(initial = 0)
) { ) {
@@ -24,6 +76,8 @@ class AppBarHandler(
val connection = CollapsingAppBarNestedScrollConnection() val connection = CollapsingAppBarNestedScrollConnection()
val backgroundGraphicsLayerSize: MutableState<IntSize> = mutableStateOf(IntSize.Zero)
companion object { companion object {
var appBarMaxHeightPx: Int = 0 var appBarMaxHeightPx: Int = 0
} }
@@ -3,7 +3,6 @@ package chat.simplex.common.views.helpers
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.ZeroCornerSize import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.foundation.text.* import androidx.compose.foundation.text.*
import androidx.compose.material.* import androidx.compose.material.*
@@ -22,13 +21,11 @@ import androidx.compose.ui.text.input.*
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import chat.simplex.common.views.database.PassphraseStrength import chat.simplex.common.views.database.PassphraseStrength
import chat.simplex.common.views.database.validKey
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun DefaultBasicTextField( fun DefaultBasicTextField(
modifier: Modifier, modifier: Modifier,
@@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp
@Composable @Composable
fun DefaultDropdownMenu( fun DefaultDropdownMenu(
showMenu: MutableState<Boolean>, showMenu: MutableState<Boolean>,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(0.dp, 0.dp), offset: DpOffset = DpOffset(0.dp, 0.dp),
dropdownMenuItems: (@Composable () -> Unit)? dropdownMenuItems: (@Composable () -> Unit)?
) { ) {
@@ -23,7 +24,7 @@ fun DefaultDropdownMenu(
DropdownMenu( DropdownMenu(
expanded = showMenu.value, expanded = showMenu.value,
onDismissRequest = { showMenu.value = false }, onDismissRequest = { showMenu.value = false },
modifier = Modifier modifier = modifier
.widthIn(min = 250.dp) .widthIn(min = 250.dp)
.background(MaterialTheme.colors.surface) .background(MaterialTheme.colors.surface)
.padding(vertical = 4.dp), .padding(vertical = 4.dp),
@@ -3,44 +3,120 @@ package chat.simplex.common.views.helpers
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.*
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.unit.Dp import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
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.unit.dp import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.CenteredRowLayout
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlin.math.absoluteValue
@Composable @Composable
fun DefaultTopAppBar( fun DefaultAppBar(
navigationButton: (@Composable RowScope.() -> Unit)? = null, navigationButton: (@Composable RowScope.() -> Unit)? = null,
title: (@Composable () -> Unit)?, title: (@Composable () -> Unit)? = null,
fixedTitleText: String? = null,
onTitleClick: (() -> Unit)? = null, onTitleClick: (() -> Unit)? = null,
showSearch: Boolean, onTop: Boolean,
onSearchValueChanged: (String) -> Unit, showSearch: Boolean = false,
buttons: List<@Composable RowScope.() -> Unit> = emptyList(), searchAlwaysVisible: Boolean = false,
onSearchValueChanged: (String) -> Unit = {},
buttons: @Composable RowScope.() -> Unit = {},
) { ) {
// If I just disable clickable modifier when don't need it, it will stop passing clicks to search. Replacing the whole modifier // If I just disable clickable modifier when don't need it, it will stop passing clicks to search. Replacing the whole modifier
val modifier = if (!showSearch) { val modifier = if (!showSearch) {
Modifier.clickable(enabled = onTitleClick != null, onClick = onTitleClick ?: { }) Modifier.clickable(enabled = onTitleClick != null, onClick = onTitleClick ?: { })
} else Modifier } else Modifier.imePadding()
TopAppBar( val themeBackgroundMix = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f)
modifier = modifier, val prefAlpha = remember { appPrefs.inAppBarsAlpha.state }
title = { val handler = LocalAppBarHandler.current
if (!showSearch) { val connection = LocalAppBarHandler.current?.connection
title?.invoke() val titleText = remember(handler?.title?.value, fixedTitleText) {
if (fixedTitleText != null) {
mutableStateOf(fixedTitleText)
} else { } else {
SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = false, onValueChange = onSearchValueChanged) handler?.title ?: mutableStateOf("")
}
}
val keyboardInset = WindowInsets.ime
Box(modifier) {
val density = LocalDensity.current
val blurRadius = remember { appPrefs.appearanceBarsBlurRadius.state }
Box(Modifier
.matchParentSize()
.blurredBackgroundModifier(keyboardInset, handler, blurRadius, prefAlpha, handler?.keyboardCoversBar == true, onTop, density)
.drawWithCache {
// store it as a variable, don't put it inside if without holding it here. Compiler don't see it changes otherwise
val alpha = prefAlpha.value
val backgroundColor = if (title != null || fixedTitleText != null || connection == null || !onTop) {
themeBackgroundMix.copy(alpha)
} else {
themeBackgroundMix.copy(topTitleAlpha(false, connection))
}
onDrawBehind {
drawRect(backgroundColor)
}
}
)
Box(
Modifier
.fillMaxWidth()
.then(if (!onTop) Modifier.navigationBarsPadding() else Modifier)
.heightIn(min = AppBarHeight * fontSizeSqrtMultiplier)
) {
AppBar(
title = {
if (showSearch) {
SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = searchAlwaysVisible, reducedCloseButtonPadding = 12.dp, onValueChange = onSearchValueChanged)
} else if (title != null) {
title()
} else if (titleText.value.isNotEmpty() && connection != null) {
Row(
Modifier
.graphicsLayer {
alpha = if (fixedTitleText != null) 1f else topTitleAlpha(true, connection)
}
) {
Text(
titleText.value,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
} }
}, },
backgroundColor = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f),
navigationIcon = navigationButton, navigationIcon = navigationButton,
buttons = if (!showSearch) buttons else emptyList(), buttons = if (!showSearch) buttons else {{}},
centered = !showSearch, centered = !showSearch && (title != null || !onTop),
onTop = onTop,
)
AppBarDivider(onTop, title != null || fixedTitleText != null, connection)
}
}
}
@Composable
fun CallAppBar(
title: @Composable () -> Unit,
onBack: () -> Unit
) {
AppBar(
title,
navigationIcon = { NavigationButtonBack(tintColor = Color(0xFFFFFFD8), onButtonClicked = onBack) },
centered = false,
onTop = true
) )
} }
@@ -83,58 +159,107 @@ fun NavigationButtonMenu(onButtonClicked: () -> Unit) {
} }
@Composable @Composable
private fun TopAppBar( private fun BoxScope.AppBarDivider(onTop: Boolean, fixedAlpha: Boolean, connection: CollapsingAppBarNestedScrollConnection?) {
if (connection != null) {
Divider(
Modifier
.align(if (onTop) Alignment.BottomStart else Alignment.TopStart)
.graphicsLayer {
alpha = if (!onTop || fixedAlpha) 1f else topTitleAlpha(false, connection, 1f)
}
)
} else {
Divider(Modifier.align(if (onTop) Alignment.BottomStart else Alignment.TopStart))
}
}
@Composable
private fun AppBar(
title: @Composable () -> Unit, title: @Composable () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
navigationIcon: @Composable (RowScope.() -> Unit)? = null, navigationIcon: @Composable (RowScope.() -> Unit)? = null,
buttons: List<@Composable RowScope.() -> Unit> = emptyList(), buttons: @Composable RowScope.() -> Unit = {},
backgroundColor: Color = MaterialTheme.colors.primarySurface,
centered: Boolean, centered: Boolean,
onTop: Boolean,
) { ) {
Box( val adjustedModifier = modifier
modifier .then(if (onTop) Modifier.statusBarsPadding() else Modifier)
.fillMaxWidth()
.height(AppBarHeight * fontSizeSqrtMultiplier) .height(AppBarHeight * fontSizeSqrtMultiplier)
.background(backgroundColor) .fillMaxWidth()
.padding(horizontal = 4.dp), .padding(horizontal = AppBarHorizontalPadding)
contentAlignment = Alignment.CenterStart, if (centered) {
AppBarCenterAligned(adjustedModifier, title, navigationIcon, buttons)
} else {
AppBarStartAligned(adjustedModifier, title, navigationIcon, buttons)
}
}
@Composable
private fun AppBarStartAligned(
modifier: Modifier,
title: @Composable () -> Unit,
navigationIcon: @Composable (RowScope.() -> Unit)? = null,
buttons: @Composable RowScope.() -> Unit
) {
Row(
modifier,
verticalAlignment = Alignment.CenterVertically
) { ) {
if (navigationIcon != null) { if (navigationIcon != null) {
Row( navigationIcon()
Modifier Spacer(Modifier.width(AppBarHorizontalPadding))
.fillMaxHeight() } else {
.width(TitleInsetWithIcon - AppBarHorizontalPadding), Spacer(Modifier.width(DEFAULT_PADDING))
verticalAlignment = Alignment.CenterVertically, }
content = navigationIcon Row(Modifier
) .weight(1f)
.padding(end = DEFAULT_PADDING_HALF)
) {
title()
} }
Row( Row(
Modifier
.fillMaxHeight()
.fillMaxWidth(),
horizontalArrangement = Arrangement.End, horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
buttons.forEach { it() } buttons()
}
val startPadding = if (navigationIcon != null) TitleInsetWithIcon else TitleInsetWithoutIcon
val endPadding = (buttons.size * 50f).dp
Box(
Modifier
.fillMaxWidth()
.padding(
start = if (centered) kotlin.math.max(startPadding.value, endPadding.value).dp else startPadding,
end = if (centered) kotlin.math.max(startPadding.value, endPadding.value).dp else endPadding
),
contentAlignment = Alignment.Center
) {
title()
} }
} }
} }
@Composable
private fun AppBarCenterAligned(
modifier: Modifier,
title: @Composable () -> Unit,
navigationIcon: @Composable (RowScope.() -> Unit)? = null,
buttons: @Composable RowScope.() -> Unit,
) {
CenteredRowLayout(modifier) {
if (navigationIcon != null) {
Row(
Modifier.padding(end = AppBarHorizontalPadding),
verticalAlignment = Alignment.CenterVertically,
content = navigationIcon
)
} else {
Spacer(Modifier)
}
Row(
Modifier.padding(end = DEFAULT_PADDING_HALF)
) {
title()
}
Row(
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
buttons()
}
}
}
private fun topTitleAlpha(text: Boolean, connection: CollapsingAppBarNestedScrollConnection, alpha: Float = appPrefs.inAppBarsAlpha.get()) =
if (connection.appBarOffset.absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 0f
else ((-connection.appBarOffset * 1.5f) / (AppBarHandler.appBarMaxHeightPx)).coerceIn(0f, if (text) 1f else alpha)
val AppBarHeight = 56.dp val AppBarHeight = 56.dp
val AppBarHorizontalPadding = 4.dp val AppBarHorizontalPadding = 2.dp
val BottomAppBarHeight = 60.dp
private val TitleInsetWithoutIcon = DEFAULT_PADDING - AppBarHorizontalPadding
val TitleInsetWithIcon = 72.dp
@@ -6,7 +6,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.ripple.rememberRipple
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -107,6 +106,7 @@ fun <T> ExposedDropDownSettingWithIcon(
expanded.value = !expanded.value && enabled.value expanded.value = !expanded.value && enabled.value
} }
) { ) {
val ripple = remember { ripple(bounded = false, radius = boxSize / 2, color = background.lighter(0.1f)) }
Box( Box(
Modifier Modifier
.background(background, CircleShape) .background(background, CircleShape)
@@ -115,7 +115,7 @@ fun <T> ExposedDropDownSettingWithIcon(
onClick = {}, onClick = {},
role = Role.Button, role = Role.Button,
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = boxSize / 2, color = background.lighter(0.1f)), indication = ripple,
enabled = enabled.value enabled = enabled.value
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -6,12 +6,14 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chatlist.StatusBarBackground
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.min import kotlin.math.min
@@ -21,24 +23,40 @@ import kotlin.math.sqrt
fun ModalView( fun ModalView(
close: () -> Unit, close: () -> Unit,
showClose: Boolean = true, showClose: Boolean = true,
showAppBar: Boolean = true,
enableClose: Boolean = true, enableClose: Boolean = true,
background: Color = MaterialTheme.colors.background, background: Color = Color.Unspecified,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
closeOnTop: Boolean = true, showSearch: Boolean = false,
searchAlwaysVisible: Boolean = false,
onSearchValueChanged: (String) -> Unit = {},
endButtons: @Composable RowScope.() -> Unit = {}, endButtons: @Composable RowScope.() -> Unit = {},
content: @Composable () -> Unit, content: @Composable BoxScope.() -> Unit,
) { ) {
if (showClose) { if (showClose && showAppBar) {
BackHandler(enabled = enableClose, onBack = close) BackHandler(enabled = enableClose, onBack = close)
} }
val oneHandUI = remember { appPrefs.oneHandUI.state }
Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) { Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) {
Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) { Box(if (background != Color.Unspecified) Modifier.background(background) else Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) {
if (closeOnTop) {
CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons)
}
Box(modifier = modifier) { Box(modifier = modifier) {
content() content()
} }
if (showAppBar) {
if (oneHandUI.value) {
StatusBarBackground()
}
Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) {
DefaultAppBar(
navigationButton = if (showClose) {{ NavigationButtonBack(onButtonClicked = if (enableClose) close else null) }} else null,
onTop = !oneHandUI.value,
showSearch = showSearch,
searchAlwaysVisible = searchAlwaysVisible,
onSearchValueChanged = onSearchValueChanged,
buttons = endButtons
)
}
}
} }
} }
} }
@@ -47,7 +65,7 @@ enum class ModalPlacement {
START, CENTER, END, FULLSCREEN START, CENTER, END, FULLSCREEN
} }
class ModalData() { class ModalData(val keyboardCoversBar: Boolean = true) {
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>
@@ -55,7 +73,7 @@ class ModalData() {
fun <T> stateGetOrPutNullable (key: String, default: () -> T?): MutableState<T?> = fun <T> stateGetOrPutNullable (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?>
val appBarHandler = AppBarHandler() val appBarHandler = AppBarHandler(null, null, keyboardCoversBar = keyboardCoversBar)
} }
class ModalManager(private val placement: ModalPlacement? = null) { class ModalManager(private val placement: ModalPlacement? = null) {
@@ -69,23 +87,21 @@ class ModalManager(private val placement: ModalPlacement? = null) {
private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null) private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
private var onTimePasscodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null) private var onTimePasscodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
fun showModal(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) { fun showModal(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
val data = ModalData()
showCustomModal { close -> showCustomModal { close ->
ModalView(close, showClose = showClose, closeOnTop = closeOnTop, endButtons = endButtons, content = { data.content() }) ModalView(close, showClose = showClose, endButtons = endButtons, content = { content() })
} }
} }
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) { fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
val data = ModalData()
showCustomModal { close -> showCustomModal { close ->
ModalView(close, showClose = showClose, endButtons = endButtons, closeOnTop = closeOnTop, content = { data.content(close) }) ModalView(close, showClose = showClose, endButtons = endButtons, content = { content(close) })
} }
} }
fun showCustomModal(animated: Boolean = true, modal: @Composable ModalData.(close: () -> Unit) -> Unit) { fun showCustomModal(animated: Boolean = true, keyboardCoversBar: Boolean = true, modal: @Composable ModalData.(close: () -> Unit) -> Unit) {
Log.d(TAG, "ModalManager.showCustomModal") Log.d(TAG, "ModalManager.showCustomModal")
val data = ModalData() val data = ModalData(keyboardCoversBar = keyboardCoversBar)
// Means, animation is in progress or not started yet. Do not wait until animation finishes, just remove all from screen. // Means, animation is in progress or not started yet. Do not wait until animation finishes, just remove all from screen.
// This is useful when invoking close() and ShowCustomModal one after another without delay. Otherwise, screen will hold prev view // This is useful when invoking close() and ShowCustomModal one after another without delay. Otherwise, screen will hold prev view
if (toRemove.isNotEmpty()) { if (toRemove.isNotEmpty()) {
@@ -146,9 +162,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
// Without animation // Without animation
if (modalCount.value > 0 && modalViews.lastOrNull()?.first == false) { if (modalCount.value > 0 && modalViews.lastOrNull()?.first == false) {
modalViews.lastOrNull()?.let { modalViews.lastOrNull()?.let {
CompositionLocalProvider( CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.second.appBarHandler)) {
LocalAppBarHandler provides it.second.appBarHandler
) {
it.third(it.second, ::closeModal) it.third(it.second, ::closeModal)
} }
} }
@@ -164,9 +178,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
} }
) { ) {
modalViews.getOrNull(it - 1)?.let { modalViews.getOrNull(it - 1)?.let {
CompositionLocalProvider( CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.second.appBarHandler)) {
LocalAppBarHandler provides it.second.appBarHandler
) {
it.third(it.second, ::closeModal) it.third(it.second, ::closeModal)
} }
} }
@@ -2,7 +2,7 @@ package chat.simplex.common.views.helpers
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.ZeroCornerSize import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.foundation.text.* import androidx.compose.foundation.text.*
import androidx.compose.material.* import androidx.compose.material.*
@@ -18,12 +18,9 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
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.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.* import androidx.compose.ui.text.input.*
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.*
import androidx.compose.ui.unit.sp
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -38,6 +35,7 @@ fun SearchTextField(
placeholder: String = stringResource(MR.strings.search_verb), placeholder: String = stringResource(MR.strings.search_verb),
enabled: Boolean = true, enabled: Boolean = true,
trailingContent: @Composable (() -> Unit)? = null, trailingContent: @Composable (() -> Unit)? = null,
reducedCloseButtonPadding: Dp = 0.dp,
onValueChange: (String) -> Unit onValueChange: (String) -> Unit
) { ) {
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@@ -81,15 +79,20 @@ fun SearchTextField(
) )
val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize) val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize)
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
// sizing is done differently on Android and desktop in order to have the same height of search and compose view on desktop
// see PlatformTextField.desktop + SendMsgView
val padding = if (appPlatform.isAndroid) PaddingValues() else PaddingValues(top = 3.dp, bottom = 4.dp)
BasicTextField( BasicTextField(
value = searchText.value, value = searchText.value,
modifier = modifier modifier = modifier
.background(colors.backgroundColor(enabled).value, shape) .background(colors.backgroundColor(enabled).value, shape)
.indicatorLine(enabled, false, interactionSource, colors) .indicatorLine(enabled, false, interactionSource, colors)
.focusRequester(focusRequester) .focusRequester(focusRequester)
.padding(padding)
.defaultMinSize( .defaultMinSize(
minWidth = TextFieldDefaults.MinWidth, minWidth = TextFieldDefaults.MinWidth,
minHeight = TextFieldDefaults.MinHeight minHeight = if (appPlatform.isAndroid) TextFieldDefaults.MinHeight else 0.dp
), ),
onValueChange = { onValueChange = {
searchText.value = it searchText.value = it
@@ -100,18 +103,14 @@ fun SearchTextField(
visualTransformation = VisualTransformation.None, visualTransformation = VisualTransformation.None,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
singleLine = true, singleLine = true,
textStyle = TextStyle( textStyle = textStyle,
color = MaterialTheme.colors.onBackground,
fontWeight = FontWeight.Normal,
fontSize = 15.sp
),
interactionSource = interactionSource, interactionSource = interactionSource,
decorationBox = @Composable { innerTextField -> decorationBox = @Composable { innerTextField ->
TextFieldDefaults.TextFieldDecorationBox( TextFieldDefaults.TextFieldDecorationBox(
value = searchText.value.text, value = searchText.value.text,
innerTextField = innerTextField, innerTextField = innerTextField,
placeholder = { placeholder = {
Text(placeholder, maxLines = 1, overflow = TextOverflow.Ellipsis) Text(placeholder, style = textStyle.copy(color = MaterialTheme.colors.secondary), maxLines = 1, overflow = TextOverflow.Ellipsis)
}, },
trailingIcon = if (searchText.value.text.isNotEmpty()) {{ trailingIcon = if (searchText.value.text.isNotEmpty()) {{
IconButton({ IconButton({
@@ -121,7 +120,7 @@ fun SearchTextField(
} }
searchText.value = TextFieldValue(""); searchText.value = TextFieldValue("");
onValueChange("") onValueChange("")
}) { }, Modifier.offset(x = reducedCloseButtonPadding)) {
Icon(painterResource(MR.images.ic_close), stringResource(MR.strings.icon_descr_close_button), tint = MaterialTheme.colors.primary,) Icon(painterResource(MR.images.ic_close), stringResource(MR.strings.icon_descr_close_button), tint = MaterialTheme.colors.primary,)
} }
}} else trailingContent, }} else trailingContent,
@@ -57,7 +57,6 @@ fun TextEditor(
) { ) {
val textFieldModifier = modifier val textFieldModifier = modifier
.fillMaxWidth() .fillMaxWidth()
.navigationBarsWithImePadding()
.onFocusChanged { focused = it.isFocused } .onFocusChanged { focused = it.isFocused }
.padding(10.dp) .padding(10.dp)
@@ -87,6 +86,7 @@ fun TextEditor(
enabled = true, enabled = true,
isError = false, isError = false,
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified)
) )
} }
) )
@@ -32,10 +32,7 @@ fun ModalData.UserWallpaperEditor(
globalThemeUsed: MutableState<Boolean>, globalThemeUsed: MutableState<Boolean>,
save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier
.fillMaxSize()
) {
val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } } val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } }
var showMore by remember { stateGetOrPut("showMore") { false } } var showMore by remember { stateGetOrPut("showMore") { false } }
val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } } val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } }
@@ -231,10 +228,7 @@ fun ModalData.ChatWallpaperEditor(
globalThemeUsed: MutableState<Boolean>, globalThemeUsed: MutableState<Boolean>,
save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier
.fillMaxSize()
) {
val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } } val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } }
var showMore by remember { stateGetOrPut("showMore") { false } } var showMore by remember { stateGetOrPut("showMore") { false } }
val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } } val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } }
@@ -149,9 +149,7 @@ private fun MigrateFromDeviceLayout(
) { ) {
val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) } val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) }
ColumnWithScrollBar( ColumnWithScrollBar(maxIntrinsicSize = true) {
Modifier.fillMaxSize(), maxIntrinsicSize = true
) {
AppBarTitle(stringResource(MR.strings.migrate_from_device_title)) AppBarTitle(stringResource(MR.strings.migrate_from_device_title))
SectionByState(migrationState, tempDatabaseFile.value, chatReceiver) SectionByState(migrationState, tempDatabaseFile.value, chatReceiver)
SectionBottomSpacer() SectionBottomSpacer()
@@ -162,9 +162,7 @@ private fun ModalData.MigrateToDeviceLayout(
close: () -> Unit, close: () -> Unit,
) { ) {
val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) } val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) }
ColumnWithScrollBar( ColumnWithScrollBar(maxIntrinsicSize = true) {
Modifier.fillMaxSize(), maxIntrinsicSize = true
) {
AppBarTitle(stringResource(MR.strings.migrate_to_device_title)) AppBarTitle(stringResource(MR.strings.migrate_to_device_title))
SectionByState(migrationState, tempDatabaseFile.value, chatReceiver, close) SectionByState(migrationState, tempDatabaseFile.value, chatReceiver, close)
SectionBottomSpacer() SectionBottomSpacer()
@@ -15,9 +15,7 @@ import chat.simplex.res.MR
@Composable @Composable
fun AddContactLearnMore(close: () -> Unit) { fun AddContactLearnMore(close: () -> Unit) {
ColumnWithScrollBar( ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) {
Modifier.padding(horizontal = DEFAULT_PADDING),
) {
AppBarTitle(stringResource(MR.strings.one_time_link), withPadding = false) AppBarTitle(stringResource(MR.strings.one_time_link), withPadding = false)
ReadableText(MR.strings.scan_qr_to_connect_to_contact) ReadableText(MR.strings.scan_qr_to_connect_to_contact)
ReadableText(MR.strings.if_you_cant_meet_in_person) ReadableText(MR.strings.if_you_cant_meet_in_person)
@@ -84,10 +84,9 @@ fun AddGroupLayout(
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val incognito = remember { mutableStateOf(incognitoPref.get()) } val incognito = remember { mutableStateOf(incognitoPref.get()) }
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
ModalBottomSheetLayout( ModalBottomSheetLayout(
scrimColor = Color.Black.copy(alpha = 0.12F), scrimColor = Color.Black.copy(alpha = 0.12F),
modifier = Modifier.navigationBarsWithImePadding(), modifier = Modifier.imePadding(),
sheetContent = { sheetContent = {
GetImageBottomSheet( GetImageBottomSheet(
chosenImage, chosenImage,
@@ -100,11 +99,7 @@ fun AddGroupLayout(
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
) { ) {
ModalView(close = close) { ModalView(close = close) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier
.fillMaxSize()
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(MR.strings.create_secret_group_title), hostDevice(rhId)) AppBarTitle(stringResource(MR.strings.create_secret_group_title), hostDevice(rhId))
Box( Box(
Modifier Modifier
@@ -122,7 +117,7 @@ fun AddGroupLayout(
} }
} }
} }
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Row(Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text( Text(
stringResource(MR.strings.group_display_name_field), stringResource(MR.strings.group_display_name_field),
fontSize = 16.sp fontSize = 16.sp
@@ -134,7 +129,9 @@ fun AddGroupLayout(
} }
} }
} }
Box(Modifier.padding(horizontal = DEFAULT_PADDING)) {
ProfileNameField(displayName, "", { isValidDisplayName(it.trim()) }, focusRequester) ProfileNameField(displayName, "", { isValidDisplayName(it.trim()) }, focusRequester)
}
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
SettingsActionItem( SettingsActionItem(
@@ -171,7 +168,6 @@ fun AddGroupLayout(
} }
} }
} }
}
fun canCreateProfile(displayName: String): Boolean = displayName.trim().isNotEmpty() && isValidDisplayName(displayName.trim()) fun canCreateProfile(displayName: String): Boolean = displayName.trim().isNotEmpty() && isValidDisplayName(displayName.trim())
@@ -89,7 +89,7 @@ private fun ContactConnectionInfoLayout(
SettingsActionItemWithContent( SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_theater_comedy_filled), icon = painterResource(MR.images.ic_theater_comedy_filled),
text = null, text = null,
click = { ModalManager.start.showModal { IncognitoView() } }, click = { ModalManager.end.showModal { IncognitoView() } },
iconColor = Indigo, iconColor = Indigo,
extraPadding = false extraPadding = false
) { ) {
@@ -105,9 +105,7 @@ private fun ContactConnectionInfoLayout(
} }
} }
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier,
) {
AppBarTitle( AppBarTitle(
stringResource( stringResource(
if (contactConnection.initiated) MR.strings.you_invited_a_contact if (contactConnection.initiated) MR.strings.you_invited_a_contact
@@ -1,9 +1,7 @@
package chat.simplex.common.views.newchat package chat.simplex.common.views.newchat
import SectionDivider
import SectionDividerSpaced import SectionDividerSpaced
import SectionItemView import SectionItemView
import SectionSpacer
import SectionView import SectionView
import TextIconSpaced import TextIconSpaced
import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.desktop.ui.tooling.preview.Preview
@@ -14,8 +12,7 @@ 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.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.* import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
@@ -32,41 +29,20 @@ import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chatlist.ScrollDirection import chat.simplex.common.views.chat.topPaddingToContent
import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.contacts.* import chat.simplex.common.views.contacts.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import java.net.URI
@Composable @Composable
fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) { fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) {
val oneHandUI = remember { appPrefs.oneHandUI.state } val oneHandUI = remember { appPrefs.oneHandUI.state }
val keyboardState by getKeyboardState()
val showToolbarInOneHandUI = remember { derivedStateOf { keyboardState == KeyboardState.Closed && oneHandUI.value } }
Scaffold( Box {
bottomBar = {
if (showToolbarInOneHandUI.value) {
Column {
Divider()
CloseSheetBar(
close = close,
showClose = true,
endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) },
arrangement = Arrangement.Bottom,
closeBarTitle = generalGetString(MR.strings.new_message),
barPaddingValues = PaddingValues(horizontal = 0.dp)
)
}
}
}
) {
Column(
modifier = Modifier.fillMaxSize().padding(it)
) {
val closeAll = { ModalManager.start.closeModals() } val closeAll = { ModalManager.start.closeModals() }
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
@@ -84,6 +60,14 @@ fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) {
close = close close = close
) )
} }
if (oneHandUI.value) {
Column(Modifier.align(Alignment.BottomCenter)) {
DefaultAppBar(
navigationButton = { NavigationButtonBack(onButtonClicked = close) },
fixedTitleText = generalGetString(MR.strings.new_message),
onTop = false,
)
}
} }
} }
} }
@@ -187,69 +171,6 @@ private fun ModalData.NewChatSheetLayout(
derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) } derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) }
} }
LazyColumnWithScrollBar(
Modifier.fillMaxSize(),
listState,
reverseLayout = oneHandUI.value
) {
if (!oneHandUI.value) {
item {
Box(contentAlignment = Alignment.Center) {
val bottomPadding = DEFAULT_PADDING
AppBarTitle(
stringResource(MR.strings.new_message),
hostDevice(rh?.remoteHostId),
bottomPadding = bottomPadding
)
}
}
}
stickyHeader {
Column(
Modifier
.offset {
val y = if (searchText.value.text.isEmpty()) {
val offsetMultiplier = if (oneHandUI.value) 1 else -1
if (
(oneHandUI.value && scrollDirection == ScrollDirection.Up) ||
(appPlatform.isAndroid && keyboardState == KeyboardState.Opened)
) {
0
} else if (oneHandUI.value && listState.firstVisibleItemIndex == 0) {
listState.firstVisibleItemScrollOffset
} else if (!oneHandUI.value && listState.firstVisibleItemIndex == 0) {
0
} else if (!oneHandUI.value && listState.firstVisibleItemIndex == 1) {
-listState.firstVisibleItemScrollOffset
} else {
offsetMultiplier * 1000
}
} else {
0
}
IntOffset(0, y)
}
.background(MaterialTheme.colors.background)
) {
Divider()
ContactsSearchBar(
listState = listState,
searchText = searchText,
searchShowingSimplexLink = searchShowingSimplexLink,
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
close = close,
)
if (!oneHandUI.value) {
Divider()
}
}
}
item {
if (searchText.value.text.isEmpty()) {
Spacer(Modifier.padding(bottom = 27.dp))
}
val actionButtonsOriginal = listOf( val actionButtonsOriginal = listOf(
Triple( Triple(
painterResource(MR.images.ic_add_link), painterResource(MR.images.ic_add_link),
@@ -268,10 +189,10 @@ private fun ModalData.NewChatSheetLayout(
) )
) )
val actionButtons by remember(oneHandUI.value) { @Composable
derivedStateOf { fun DeletedChatsItem(actionButtons: List<Triple<Painter, String, () -> Unit>>) {
if (oneHandUI.value) actionButtonsOriginal.asReversed() else actionButtonsOriginal if (searchText.value.text.isEmpty()) {
} Spacer(Modifier.padding(bottom = 27.dp))
} }
if (searchText.value.text.isEmpty()) { if (searchText.value.text.isEmpty()) {
@@ -294,8 +215,11 @@ private fun ModalData.NewChatSheetLayout(
ModalManager.start.showCustomModal { closeDeletedChats -> ModalManager.start.showCustomModal { closeDeletedChats ->
ModalView( ModalView(
close = closeDeletedChats, close = closeDeletedChats,
closeOnTop = !oneHandUI.value, showAppBar = !oneHandUI.value,
) { ) {
if (oneHandUI.value) {
BackHandler(onBack = closeDeletedChats)
}
DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = { DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = {
ModalManager.start.closeModals() ModalManager.start.closeModals()
}) })
@@ -316,20 +240,8 @@ private fun ModalData.NewChatSheetLayout(
} }
} }
item { @Composable
if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) { fun NoFilteredContactsItem() {
if (!oneHandUI.value) {
SectionDividerSpaced()
SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
} else {
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
}
}
}
item {
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) { if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) { Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
@@ -342,6 +254,68 @@ private fun ModalData.NewChatSheetLayout(
} }
} }
@Composable
fun OneHandLazyColumn() {
val blankSpaceSize = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier
LazyColumnWithScrollBar(
state = listState,
reverseLayout = oneHandUI.value
) {
item { Spacer(Modifier.height(blankSpaceSize)) }
stickyHeader {
val scrolledSomething by remember { derivedStateOf { listState.firstVisibleItemScrollOffset > 0 || listState.firstVisibleItemIndex > 0 } }
Column(
Modifier
.zIndex(1f)
.offset {
val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened)) {
if (listState.firstVisibleItemIndex == 0) -minOf(listState.firstVisibleItemScrollOffset, blankSpaceSize.roundToPx())
else -blankSpaceSize.roundToPx()
} else {
when (listState.firstVisibleItemIndex) {
0 -> 0
1 -> listState.firstVisibleItemScrollOffset
else -> 1000
}
}
IntOffset(0, y)
}
// show background when something is scrolled because otherwise the bar is transparent.
// not using background always because of gradient in SimpleX theme
.background(
if (scrolledSomething && (keyboardState == KeyboardState.Opened || searchText.value.text.isNotEmpty())) {
MaterialTheme.colors.background
} else {
Color.Unspecified
}
)
) {
Divider()
Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight * fontSizeSqrtMultiplier))) {
ContactsSearchBar(
listState = listState,
searchText = searchText,
searchShowingSimplexLink = searchShowingSimplexLink,
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
close = close,
)
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
}
}
}
item {
DeletedChatsItem(actionButtonsOriginal.asReversed())
}
item {
if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) {
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
}
}
item {
NoFilteredContactsItem()
}
itemsIndexed(filteredContactChats) { index, chat -> itemsIndexed(filteredContactChats) { index, chat ->
val nextChatSelected = remember(chat.id, filteredContactChats) { val nextChatSelected = remember(chat.id, filteredContactChats) {
derivedStateOf { derivedStateOf {
@@ -350,6 +324,106 @@ private fun ModalData.NewChatSheetLayout(
} }
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true) ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true)
} }
if (appPlatform.isAndroid) {
item {
Spacer(Modifier.windowInsetsTopHeight(WindowInsets.statusBars))
}
}
}
}
@Composable
fun NonOneHandLazyColumn() {
val blankSpaceSize = topPaddingToContent()
LazyColumnWithScrollBar(
Modifier.imePadding(),
state = listState,
reverseLayout = false
) {
item {
Box(Modifier.padding(top = blankSpaceSize)) {
AppBarTitle(
stringResource(MR.strings.new_message),
hostDevice(rh?.remoteHostId),
bottomPadding = DEFAULT_PADDING
)
}
}
stickyHeader {
val scrolledSomething by remember { derivedStateOf { listState.firstVisibleItemScrollOffset > 0 || listState.firstVisibleItemIndex > 0 } }
Column(
Modifier
.zIndex(1f)
.offset {
val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened)) {
if (listState.firstVisibleItemIndex == 0) (listState.firstVisibleItemScrollOffset - (listState.layoutInfo.visibleItemsInfo[0].size - blankSpaceSize.roundToPx())).coerceAtLeast(0)
else blankSpaceSize.roundToPx()
} else {
when (listState.firstVisibleItemIndex) {
0 -> 0
1 -> -listState.firstVisibleItemScrollOffset
else -> -1000
}
}
IntOffset(0, y)
}
// show background when something is scrolled because otherwise the bar is transparent.
// not using background always because of gradient in SimpleX theme
.background(
if (scrolledSomething && (keyboardState == KeyboardState.Opened || searchText.value.text.isNotEmpty())) {
MaterialTheme.colors.background
} else {
Color.Unspecified
}
)
) {
Divider()
ContactsSearchBar(
listState = listState,
searchText = searchText,
searchShowingSimplexLink = searchShowingSimplexLink,
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
close = close,
)
Divider()
}
}
item {
DeletedChatsItem(actionButtonsOriginal)
}
item {
if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) {
SectionDividerSpaced()
SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
}
}
item {
NoFilteredContactsItem()
}
itemsIndexed(filteredContactChats) { index, chat ->
val nextChatSelected = remember(chat.id, filteredContactChats) {
derivedStateOf {
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
}
}
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true)
}
if (appPlatform.isAndroid) {
item {
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
}
}
}
}
Box {
if (oneHandUI.value) {
OneHandLazyColumn()
StatusBarBackground()
} else {
NonOneHandLazyColumn()
NavigationBarBackground(oneHandUI.value, true)
}
} }
} }
@@ -554,26 +628,7 @@ private fun contactTypesSearchTargets(baseContactTypes: List<ContactType>, searc
@Composable @Composable
private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Unit, close: () -> Unit) { private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Unit, close: () -> Unit) {
val oneHandUI = remember { appPrefs.oneHandUI.state } val oneHandUI = remember { appPrefs.oneHandUI.state }
val keyboardState by getKeyboardState() Box {
val showToolbarInOneHandUI = remember { derivedStateOf { keyboardState == KeyboardState.Closed && oneHandUI.value } }
Scaffold(
bottomBar = {
if (showToolbarInOneHandUI.value) {
Column {
Divider()
CloseSheetBar(
close = closeDeletedChats,
showClose = true,
endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) },
arrangement = Arrangement.Bottom,
closeBarTitle = generalGetString(MR.strings.deleted_chats),
barPaddingValues = PaddingValues(horizontal = 0.dp)
)
}
}
}
) { contentPadding ->
val listState = remember { appBarHandler.listState } val listState = remember { appBarHandler.listState }
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
val searchShowingSimplexLink = remember { mutableStateOf(false) } val searchShowingSimplexLink = remember { mutableStateOf(false) }
@@ -590,9 +645,14 @@ private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats
contactChats = allChats contactChats = allChats
) )
Box {
val topPaddingToContent = topPaddingToContent()
LazyColumnWithScrollBar( LazyColumnWithScrollBar(
Modifier.fillMaxSize(), if (!oneHandUI.value) Modifier.imePadding() else Modifier,
contentPadding = contentPadding, contentPadding = PaddingValues(
top = if (!oneHandUI.value) topPaddingToContent else 0.dp,
bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp
),
reverseLayout = oneHandUI.value, reverseLayout = oneHandUI.value,
) { ) {
item { item {
@@ -610,7 +670,6 @@ private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats
item { item {
if (!oneHandUI.value) { if (!oneHandUI.value) {
Divider() Divider()
}
ContactsSearchBar( ContactsSearchBar(
listState = listState, listState = listState,
searchText = searchText, searchText = searchText,
@@ -618,6 +677,18 @@ private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
close = close, close = close,
) )
} else {
Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) {
ContactsSearchBar(
listState = listState,
searchText = searchText,
searchShowingSimplexLink = searchShowingSimplexLink,
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
close = close,
)
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
}
}
Divider() Divider()
} }
@@ -642,6 +713,26 @@ private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats
} }
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = false) ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = false)
} }
if (appPlatform.isAndroid) {
item {
Spacer(if (oneHandUI.value) Modifier.windowInsetsTopHeight(WindowInsets.statusBars) else Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
}
}
}
if (oneHandUI.value) {
StatusBarBackground()
} else {
NavigationBarBackground(oneHandUI.value, true)
}
}
if (oneHandUI.value) {
Column(Modifier.align(Alignment.BottomCenter)) {
DefaultAppBar(
navigationButton = { NavigationButtonBack(onButtonClicked = closeDeletedChats) },
fixedTitleText = generalGetString(MR.strings.deleted_chats),
onTop = false,
)
}
} }
} }
} }
@@ -29,10 +29,12 @@ 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.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.model.ChatModel.withChats import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.topPaddingToContent
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.* import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR import chat.simplex.res.MR
@@ -398,8 +400,12 @@ fun ActiveProfilePicker(
.fillMaxSize() .fillMaxSize()
.alpha(if (progressByTimeout) 0.6f else 1f) .alpha(if (progressByTimeout) 0.6f else 1f)
) { ) {
LazyColumnWithScrollBar(userScrollEnabled = !switchingProfile.value) { LazyColumnWithScrollBar(Modifier.padding(top = topPaddingToContent()), userScrollEnabled = !switchingProfile.value) {
item { item {
val oneHandUI = remember { appPrefs.oneHandUI.state }
if (oneHandUI.value) {
Spacer(Modifier.padding(top = DEFAULT_PADDING + 5.dp))
}
AppBarTitle(stringResource(MR.strings.select_chat_profile), hostDevice(rhId), bottomPadding = DEFAULT_PADDING) AppBarTitle(stringResource(MR.strings.select_chat_profile), hostDevice(rhId), bottomPadding = DEFAULT_PADDING)
} }
val activeProfile = filteredProfiles.firstOrNull { it.activeUser } val activeProfile = filteredProfiles.firstOrNull { it.activeUser }
@@ -434,6 +440,9 @@ fun ActiveProfilePicker(
ProfilePickerUserOption(p) ProfilePickerUserOption(p)
} }
} }
item {
Spacer(Modifier.imePadding().padding(bottom = DEFAULT_BOTTOM_PADDING))
}
} }
} }
if (progressByTimeout) { if (progressByTimeout) {
@@ -472,13 +481,13 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection
end = 16.dp end = 16.dp
), ),
click = { click = {
ModalManager.start.showCustomModal { close -> ModalManager.start.showCustomModal(keyboardCoversBar = false) { close ->
val search = rememberSaveable { mutableStateOf("") } val search = rememberSaveable { mutableStateOf("") }
ModalView( ModalView(
{ close() }, { close() },
endButtons = { showSearch = true,
SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } searchAlwaysVisible = true,
}, onSearchValueChanged = { search.value = it },
content = { content = {
ActiveProfilePicker( ActiveProfilePicker(
search = search, search = search,
@@ -616,6 +625,7 @@ fun LinkTextView(link: String, share: Boolean) {
enabled = false, enabled = false,
isError = false, isError = false,
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified)
) )
}) })
} }
@@ -76,15 +76,9 @@ private fun CreateSimpleXAddressLayout(
createAddress: () -> Unit, createAddress: () -> Unit,
nextStep: () -> Unit, nextStep: () -> Unit,
) { ) {
val handler = remember { AppBarHandler() } CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
CompositionLocalProvider(
LocalAppBarHandler provides handler
) {
ModalView({}, showClose = false) { ModalView({}, showClose = false) {
ColumnWithScrollBar( ColumnWithScrollBar(
Modifier
.fillMaxSize()
.themedBackground(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
AppBarTitle(stringResource(MR.strings.simplex_address)) AppBarTitle(stringResource(MR.strings.simplex_address))
@@ -23,11 +23,7 @@ import dev.icerock.moko.resources.StringResource
@Composable @Composable
fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>? = null) { fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>? = null) {
ColumnWithScrollBar( ColumnWithScrollBar(Modifier.padding(DEFAULT_PADDING)) {
Modifier
.fillMaxWidth()
.padding(DEFAULT_PADDING),
) {
AppBarTitle(stringResource(MR.strings.how_simplex_works), withPadding = false) AppBarTitle(stringResource(MR.strings.how_simplex_works), withPadding = false)
ReadableText(MR.strings.many_people_asked_how_can_it_deliver) ReadableText(MR.strings.many_people_asked_how_can_it_deliver)
ReadableText(MR.strings.to_protect_privacy_simplex_has_ids_for_queues) ReadableText(MR.strings.to_protect_privacy_simplex_has_ids_for_queues)
@@ -7,21 +7,16 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.BackHandler
import chat.simplex.common.platform.chatModel import chat.simplex.common.platform.chatModel
import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.ui.theme.themedBackground
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.remote.AddingMobileDevice import chat.simplex.common.views.remote.AddingMobileDevice
import chat.simplex.common.views.remote.DeviceNameField import chat.simplex.common.views.remote.DeviceNameField
import chat.simplex.common.views.usersettings.PreferenceToggle import chat.simplex.common.views.usersettings.PreferenceToggle
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
@Composable @Composable
@@ -59,14 +54,11 @@ private fun LinkAMobileLayout(
staleQrCode: MutableState<Boolean>, staleQrCode: MutableState<Boolean>,
updateDeviceName: (String) -> Unit, updateDeviceName: (String) -> Unit,
) { ) {
Column(Modifier.themedBackground()) { ModalView({ appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) }) {
CloseSheetBar(close = { Column(Modifier.fillMaxSize().padding(top = AppBarHeight * fontSizeSqrtMultiplier)) {
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) Box(Modifier.align(Alignment.CenterHorizontally)) {
})
BackHandler(onBack = {
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
})
AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles))
}
Row(Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING * 2), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING * 2), verticalAlignment = Alignment.CenterVertically) {
Column( Column(
Modifier.weight(0.3f), Modifier.weight(0.3f),
@@ -93,3 +85,4 @@ private fun LinkAMobileLayout(
} }
} }
} }
}
@@ -25,16 +25,9 @@ import chat.simplex.res.MR
@Composable @Composable
fun SetNotificationsMode(m: ChatModel) { fun SetNotificationsMode(m: ChatModel) {
val handler = remember { AppBarHandler() } CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
CompositionLocalProvider(
LocalAppBarHandler provides handler
) {
ModalView({}, showClose = false) { ModalView({}, showClose = false) {
ColumnWithScrollBar( ColumnWithScrollBar(Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) {
modifier = Modifier
.fillMaxSize()
.themedBackground()
) {
Box(Modifier.align(Alignment.CenterHorizontally)) { Box(Modifier.align(Alignment.CenterHorizontally)) {
AppBarTitle(stringResource(MR.strings.onboarding_notifications_mode_title)) AppBarTitle(stringResource(MR.strings.onboarding_notifications_mode_title))
} }
@@ -104,13 +104,10 @@ private fun SetupDatabasePassphraseLayout(
onConfirmEncrypt: () -> Unit, onConfirmEncrypt: () -> Unit,
nextStep: () -> Unit, nextStep: () -> Unit,
) { ) {
val handler = remember { AppBarHandler() } CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
CompositionLocalProvider(
LocalAppBarHandler provides handler
) {
ModalView({}, showClose = false) { ModalView({}, showClose = false) {
ColumnWithScrollBar( ColumnWithScrollBar(
Modifier.fillMaxSize().themedBackground().padding(bottom = DEFAULT_PADDING * 2), Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer).padding(bottom = DEFAULT_PADDING * 2),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
AppBarTitle(stringResource(MR.strings.setup_database_passphrase)) AppBarTitle(stringResource(MR.strings.setup_database_passphrase))
@@ -31,6 +31,7 @@ import dev.icerock.moko.resources.StringResource
@Composable @Composable
fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) { fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) {
if (onboarding) { if (onboarding) {
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
ModalView({}, showClose = false, endButtons = { ModalView({}, showClose = false, endButtons = {
IconButton({ ModalManager.fullscreen.showModal { HowItWorks(chatModel.currentUser.value, null) } }) { IconButton({ ModalManager.fullscreen.showModal { HowItWorks(chatModel.currentUser.value, null) } }) {
Icon(painterResource(MR.images.ic_info), null, Modifier.size(28.dp), tint = MaterialTheme.colors.primary) Icon(painterResource(MR.images.ic_info), null, Modifier.size(28.dp), tint = MaterialTheme.colors.primary)
@@ -41,6 +42,7 @@ fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) {
onboardingStage = chatModel.controller.appPrefs.onboardingStage onboardingStage = chatModel.controller.appPrefs.onboardingStage
) )
} }
}
} else { } else {
SimpleXInfoLayout( SimpleXInfoLayout(
user = chatModel.currentUser.value, user = chatModel.currentUser.value,
@@ -56,7 +58,6 @@ fun SimpleXInfoLayout(
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar(
Modifier Modifier
.fillMaxSize()
.padding(horizontal = DEFAULT_PADDING), .padding(horizontal = DEFAULT_PADDING),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
@@ -119,11 +119,10 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
ModalView(close = close) { ModalView(close = close) {
ColumnWithScrollBar( ColumnWithScrollBar(
Modifier Modifier
.fillMaxSize()
.padding(horizontal = DEFAULT_PADDING), .padding(horizontal = DEFAULT_PADDING),
verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING.times(0.75f)) verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING.times(0.75f))
) { ) {
AppBarTitle(String.format(generalGetString(MR.strings.new_in_version), v.version), bottomPadding = DEFAULT_PADDING) AppBarTitle(String.format(generalGetString(MR.strings.new_in_version), v.version), withPadding = false, bottomPadding = DEFAULT_PADDING)
v.features.forEach { feature -> v.features.forEach { feature ->
if (feature.show) { if (feature.show) {
@@ -74,9 +74,7 @@ private fun ConnectDesktopLayout(deviceName: String, close: () -> Unit) {
val sessionAddress = remember { mutableStateOf("") } val sessionAddress = remember { mutableStateOf("") }
val remoteCtrls = remember { mutableStateListOf<RemoteCtrlInfo>() } val remoteCtrls = remember { mutableStateListOf<RemoteCtrlInfo>() }
val session = remember { chatModel.remoteCtrlSession }.value val session = remember { chatModel.remoteCtrlSession }.value
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxWidth(),
) {
val discovery = if (session == null) null else session.sessionState is UIRemoteCtrlSessionState.Searching val discovery = if (session == null) null else session.sessionState is UIRemoteCtrlSessionState.Searching
if (discovery == true || (discovery == null && !showConnectScreen.value)) { if (discovery == true || (discovery == null && !showConnectScreen.value)) {
SearchingDesktop(deviceName, remoteCtrls) SearchingDesktop(deviceName, remoteCtrls)
@@ -408,9 +406,7 @@ private fun DesktopAddressView(sessionAddress: MutableState<String>) {
@Composable @Composable
private fun LinkedDesktopsView(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) { private fun LinkedDesktopsView(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.linked_desktops)) AppBarTitle(stringResource(MR.strings.linked_desktops))
SectionView(stringResource(MR.strings.desktop_devices).uppercase()) { SectionView(stringResource(MR.strings.desktop_devices).uppercase()) {
remoteCtrls.forEach { rc -> remoteCtrls.forEach { rc ->
@@ -89,7 +89,7 @@ fun ConnectMobileLayout(
connectDesktop: () -> Unit, connectDesktop: () -> Unit,
deleteHost: (RemoteHostInfo) -> Unit, deleteHost: (RemoteHostInfo) -> Unit,
) { ) {
ColumnWithScrollBar(Modifier.fillMaxWidth()) { ColumnWithScrollBar {
AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles))
SectionView(generalGetString(MR.strings.this_device_name).uppercase()) { SectionView(generalGetString(MR.strings.this_device_name).uppercase()) {
DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) } DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) }
@@ -176,7 +176,15 @@ private fun ConnectMobileViewLayout(
refreshQrCode: () -> Unit = {}, refreshQrCode: () -> Unit = {},
UnderQrLayout: @Composable () -> Unit = {}, UnderQrLayout: @Composable () -> Unit = {},
) { ) {
ColumnWithScrollBar(Modifier.fillMaxWidth()) { @Composable
fun ScrollableLayout(content: @Composable ColumnScope.() -> Unit) {
if (LocalAppBarHandler.current != null) {
ColumnWithScrollBar(content = content)
} else {
ColumnWithScrollBarNoAppBar(content = content)
}
}
ScrollableLayout {
if (title != null) { if (title != null) {
AppBarTitle(title) AppBarTitle(title)
} }
@@ -202,10 +202,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
) { ) {
val secondsLabel = stringResource(MR.strings.network_option_seconds_label) val secondsLabel = stringResource(MR.strings.network_option_seconds_label)
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier
.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.network_settings_title)) AppBarTitle(stringResource(MR.strings.network_settings_title))
if (currentRemoteHost == null) { if (currentRemoteHost == null) {
@@ -328,9 +325,7 @@ private fun SMPProxyModePicker(
icon = painterResource(MR.images.ic_settings_ethernet), icon = painterResource(MR.images.ic_settings_ethernet),
onSelected = { onSelected = {
showModal { showModal {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.network_smp_proxy_mode_private_routing)) AppBarTitle(stringResource(MR.strings.network_smp_proxy_mode_private_routing))
SectionViewSelectableCards(null, smpProxyMode, values, updateSMPProxyMode) SectionViewSelectableCards(null, smpProxyMode, values, updateSMPProxyMode)
} }
@@ -365,9 +360,7 @@ private fun SMPProxyFallbackPicker(
enabled = enabled, enabled = enabled,
onSelected = { onSelected = {
showModal { showModal {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.network_smp_proxy_fallback_allow_downgrade)) AppBarTitle(stringResource(MR.strings.network_smp_proxy_fallback_allow_downgrade))
SectionViewSelectableCards(null, smpProxyFallback, values, updateSMPProxyFallback) SectionViewSelectableCards(null, smpProxyFallback, values, updateSMPProxyFallback)
} }
@@ -4,9 +4,11 @@ import SectionBottomSpacer
import SectionDividerSpaced import SectionDividerSpaced
import SectionItemView import SectionItemView
import SectionItemViewSpaceBetween import SectionItemViewSpaceBetween
import SectionItemViewWithoutMinPadding
import SectionSpacer import SectionSpacer
import SectionView import SectionView
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.* import androidx.compose.foundation.lazy.grid.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@@ -39,6 +41,7 @@ import chat.simplex.common.views.chat.item.msgTailWidthDp
import chat.simplex.res.MR import chat.simplex.res.MR
import com.godaddy.android.colorpicker.ClassicColorPicker import com.godaddy.android.colorpicker.ClassicColorPicker
import com.godaddy.android.colorpicker.HsvColor import com.godaddy.android.colorpicker.HsvColor
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@@ -85,21 +88,107 @@ object AppearanceScope {
} }
} }
@Composable
fun AppToolbarsSection() {
BoxWithConstraints {
SectionView(stringResource(MR.strings.appearance_app_toolbars).uppercase()) {
SectionItemViewWithoutMinPadding {
Box(Modifier.weight(1f)) {
Text(
stringResource(MR.strings.appearance_in_app_bars_alpha),
Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
appPrefs.inAppBarsAlpha.set(appPrefs.inAppBarsDefaultAlpha)
},
maxLines = 1
)
}
Spacer(Modifier.padding(end = 10.dp))
Slider(
(1 - remember { appPrefs.inAppBarsAlpha.state }.value).coerceIn(0f, 0.5f),
onValueChange = {
val diff = it % 0.025f
appPrefs.inAppBarsAlpha.set(1f - (String.format(Locale.US, "%.3f", it + (if (diff >= 0.0125f) -diff + 0.025f else -diff)).toFloatOrNull() ?: 1f))
},
Modifier.widthIn(max = (this@BoxWithConstraints.maxWidth - DEFAULT_PADDING * 2) * 0.618f),
valueRange = 0f..0.5f,
steps = 21,
colors = SliderDefaults.colors(
activeTickColor = Color.Transparent,
inactiveTickColor = Color.Transparent,
)
)
}
// In Android in OneHandUI there is a problem with setting initial value of blur if it was 0 before entering the screen.
// So doing in two steps works ok
fun saveBlur(value: Int) {
val oneHandUI = appPrefs.oneHandUI.get()
val pref = appPrefs.appearanceBarsBlurRadius
if (appPlatform.isAndroid && oneHandUI && pref.get() == 0) {
pref.set(if (value > 2) value - 1 else value + 1)
withApi {
delay(50)
pref.set(value)
}
} else {
pref.set(value)
}
}
val blur = remember { appPrefs.appearanceBarsBlurRadius.state }
if (appPrefs.deviceSupportsBlur || blur.value > 0) {
SectionItemViewWithoutMinPadding {
Box(Modifier.weight(1f)) {
Text(
stringResource(MR.strings.appearance_bars_blur_radius),
Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
saveBlur(50)
},
maxLines = 1
)
}
Spacer(Modifier.padding(end = 10.dp))
Slider(
blur.value.toFloat() / 100f,
onValueChange = {
val diff = it % 0.05f
saveBlur(((String.format(Locale.US, "%.2f", it + (if (diff >= 0.025f) -diff + 0.05f else -diff)).toFloatOrNull() ?: 1f) * 100).toInt())
},
Modifier.widthIn(max = (this@BoxWithConstraints.maxWidth - DEFAULT_PADDING * 2) * 0.618f),
valueRange = 0f..1f,
steps = 21,
colors = SliderDefaults.colors(
activeTickColor = Color.Transparent,
inactiveTickColor = Color.Transparent,
)
)
}
}
}
}
}
@Composable @Composable
fun MessageShapeSection() { fun MessageShapeSection() {
SectionView(stringResource(MR.strings.settings_section_title_message_shape).uppercase(), contentPadding = PaddingValues()) { BoxWithConstraints {
Row(modifier = Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING + 4.dp ) ,verticalAlignment = Alignment.CenterVertically) { SectionView(stringResource(MR.strings.settings_section_title_message_shape).uppercase()) {
Text(stringResource(MR.strings.settings_message_shape_corner), color = colors.onBackground) SectionItemViewWithoutMinPadding {
Text(stringResource(MR.strings.settings_message_shape_corner), Modifier.weight(1f))
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(10.dp))
Slider( Slider(
remember { appPreferences.chatItemRoundness.state }.value, remember { appPreferences.chatItemRoundness.state }.value,
valueRange = 0f..1f,
steps = 20,
onValueChange = { onValueChange = {
val diff = it % 0.05f val diff = it % 0.05f
appPreferences.chatItemRoundness.set(it + (if (diff >= 0.025f) -diff + 0.05f else -diff)) appPreferences.chatItemRoundness.set(it + (if (diff >= 0.025f) -diff + 0.05f else -diff))
saveThemeToDatabase(null) saveThemeToDatabase(null)
}, },
Modifier.widthIn(max = (this@BoxWithConstraints.maxWidth - DEFAULT_PADDING * 2) * 0.618f),
valueRange = 0f..1f,
steps = 20,
colors = SliderDefaults.colors( colors = SliderDefaults.colors(
activeTickColor = Color.Transparent, activeTickColor = Color.Transparent,
inactiveTickColor = Color.Transparent, inactiveTickColor = Color.Transparent,
@@ -109,13 +198,14 @@ object AppearanceScope {
SettingsPreferenceItem(icon = null, stringResource(MR.strings.settings_message_shape_tail), appPreferences.chatItemTail) SettingsPreferenceItem(icon = null, stringResource(MR.strings.settings_message_shape_tail), appPreferences.chatItemTail)
} }
} }
}
@Composable @Composable
fun FontScaleSection() { fun FontScaleSection() {
val localFontScale = remember { mutableStateOf(appPrefs.fontScale.get()) } val localFontScale = remember { mutableStateOf(appPrefs.fontScale.get()) }
SectionView(stringResource(MR.strings.appearance_font_size).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { SectionView(stringResource(MR.strings.appearance_font_size).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.size(60.dp) Box(Modifier.size(50.dp)
.background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22)) .background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22))
.clip(RoundedCornerShape(percent = 22)) .clip(RoundedCornerShape(percent = 22))
.clickable { .clickable {
@@ -129,7 +219,7 @@ object AppearanceScope {
Text("Aa", color = if (localFontScale.value == 1f) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground) Text("Aa", color = if (localFontScale.value == 1f) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground)
} }
} }
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(15.dp))
// Text("${(localFontScale.value * 100).roundToInt()}%", Modifier.width(70.dp), textAlign = TextAlign.Center, fontSize = 12.sp) // Text("${(localFontScale.value * 100).roundToInt()}%", Modifier.width(70.dp), textAlign = TextAlign.Center, fontSize = 12.sp)
if (appPlatform.isAndroid) { if (appPlatform.isAndroid) {
Slider( Slider(
@@ -185,7 +275,7 @@ object AppearanceScope {
Column(Modifier Column(Modifier
.drawWithCache { .drawWithCache {
if (wallpaperImage != null && wallpaperType != null && backgroundColor != null && tintColor != null) { if (wallpaperImage != null && wallpaperType != null && backgroundColor != null && tintColor != null) {
chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor, null, null)
} else { } else {
onDrawBehind { onDrawBehind {
drawRect(themeBackgroundColor) drawRect(themeBackgroundColor)
@@ -514,9 +604,7 @@ object AppearanceScope {
@Composable @Composable
fun CustomizeThemeView(onChooseType: (WallpaperType?) -> Unit) { fun CustomizeThemeView(onChooseType: (WallpaperType?) -> Unit) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxWidth(),
) {
val currentTheme by CurrentColors.collectAsState() val currentTheme by CurrentColors.collectAsState()
AppBarTitle(stringResource(MR.strings.customize_theme_title)) AppBarTitle(stringResource(MR.strings.customize_theme_title))
@@ -909,10 +997,7 @@ object AppearanceScope {
currentColors: () -> ThemeManager.ActiveTheme, currentColors: () -> ThemeManager.ActiveTheme,
onColorChange: (Color?) -> Unit, onColorChange: (Color?) -> Unit,
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar(Modifier.imePadding()) {
Modifier
.fillMaxWidth()
) {
AppBarTitle(name.text) AppBarTitle(name.text)
val supportedLiveChange = name in listOf(ThemeColor.SECONDARY, ThemeColor.BACKGROUND, ThemeColor.SURFACE, ThemeColor.RECEIVED_MESSAGE, ThemeColor.SENT_MESSAGE, ThemeColor.SENT_QUOTE, ThemeColor.WALLPAPER_BACKGROUND, ThemeColor.WALLPAPER_TINT) val supportedLiveChange = name in listOf(ThemeColor.SECONDARY, ThemeColor.BACKGROUND, ThemeColor.SURFACE, ThemeColor.RECEIVED_MESSAGE, ThemeColor.SENT_MESSAGE, ThemeColor.SENT_QUOTE, ThemeColor.WALLPAPER_BACKGROUND, ThemeColor.WALLPAPER_TINT)
@@ -36,7 +36,7 @@ fun CallSettingsLayout(
callOnLockScreen: SharedPreference<CallOnLockScreen>, callOnLockScreen: SharedPreference<CallOnLockScreen>,
editIceServers: () -> Unit, editIceServers: () -> Unit,
) { ) {
ColumnWithScrollBar(Modifier.fillMaxWidth()) { ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.your_calls)) AppBarTitle(stringResource(MR.strings.your_calls))
val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) } val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) }
SectionView(stringResource(MR.strings.settings_section_title_settings)) { SectionView(stringResource(MR.strings.settings_section_title_settings)) {
@@ -22,12 +22,10 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
@Composable @Composable
fun DeveloperView( fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
m: ChatModel,
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
) { ) {
ColumnWithScrollBar(Modifier.fillMaxWidth()) { val m = chatModel
ColumnWithScrollBar {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
AppBarTitle(stringResource(MR.strings.settings_developer_tools)) AppBarTitle(stringResource(MR.strings.settings_developer_tools))
val developerTools = m.controller.appPrefs.developerTools val developerTools = m.controller.appPrefs.developerTools
@@ -35,7 +33,7 @@ fun DeveloperView(
val unchangedHints = mutableStateOf(unchangedHintPreferences()) val unchangedHints = mutableStateOf(unchangedHintPreferences())
SectionView { SectionView {
InstallTerminalAppItem(uriHandler) InstallTerminalAppItem(uriHandler)
ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential), showCustomModal { it, close -> TerminalView(false, close) }) } ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.start.showModalCloseable { TerminalView(false) } } }
ResetHintsItem(unchangedHints) ResetHintsItem(unchangedHints)
SettingsPreferenceItem(painterResource(MR.images.ic_code), stringResource(MR.strings.show_developer_options), developerTools) SettingsPreferenceItem(painterResource(MR.images.ic_code), stringResource(MR.strings.show_developer_options), developerTools)
SectionTextFooter( SectionTextFooter(
@@ -21,11 +21,7 @@ fun HelpView(userDisplayName: String) {
@Composable @Composable
fun HelpLayout(userDisplayName: String) { fun HelpLayout(userDisplayName: String) {
ColumnWithScrollBar( ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)){
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
){
AppBarTitle(String.format(stringResource(MR.strings.personal_welcome), userDisplayName), withPadding = false) AppBarTitle(String.format(stringResource(MR.strings.personal_welcome), userDisplayName), withPadding = false)
ChatHelpView() ChatHelpView()
} }
@@ -56,10 +56,7 @@ private fun HiddenProfileLayout(
user: User, user: User,
saveProfilePassword: (String) -> Unit saveProfilePassword: (String) -> Unit
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier
.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.hide_profile)) AppBarTitle(stringResource(MR.strings.hide_profile))
SectionView(contentPadding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) { SectionView(contentPadding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
UserProfileRow(user) UserProfileRow(user)
@@ -109,7 +109,7 @@ fun NetworkAndServersView() {
toggleSocksProxy: (Boolean) -> Unit, toggleSocksProxy: (Boolean) -> Unit,
) { ) {
val m = chatModel val m = chatModel
ColumnWithScrollBar(Modifier.fillMaxWidth()) { ColumnWithScrollBar {
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) } val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) }
val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.start.showCustomModal { close -> it(close) }} val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.start.showCustomModal { close -> it(close) }}
@@ -304,10 +304,7 @@ fun SocksProxySettings(
} }
}, },
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier
.fillMaxWidth()
) {
AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings)) AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings))
SectionView(stringResource(MR.strings.network_socks_proxy).uppercase()) { SectionView(stringResource(MR.strings.network_socks_proxy).uppercase()) {
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
@@ -479,9 +476,7 @@ fun SessionModePicker(
icon = painterResource(MR.images.ic_safety_divider), icon = painterResource(MR.images.ic_safety_divider),
onSelected = { onSelected = {
showModal { showModal {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.network_session_mode_transport_isolation)) AppBarTitle(stringResource(MR.strings.network_session_mode_transport_isolation))
SectionViewSelectable(null, sessionMode, values, updateSessionMode) SectionViewSelectable(null, sessionMode, values, updateSessionMode)
} }
@@ -56,9 +56,7 @@ fun NotificationsSettingsLayout(
val modes = remember { notificationModes() } val modes = remember { notificationModes() }
val previewModes = remember { notificationPreviewModes() } val previewModes = remember { notificationPreviewModes() }
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.notifications)) AppBarTitle(stringResource(MR.strings.notifications))
SectionView(null) { SectionView(null) {
if (appPlatform == AppPlatform.ANDROID) { if (appPlatform == AppPlatform.ANDROID) {
@@ -90,9 +88,7 @@ fun NotificationsModeView(
onNotificationsModeSelected: (NotificationsMode) -> Unit, onNotificationsModeSelected: (NotificationsMode) -> Unit,
) { ) {
val modes = remember { notificationModes() } val modes = remember { notificationModes() }
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.settings_notifications_mode_title).lowercase().capitalize(Locale.current)) AppBarTitle(stringResource(MR.strings.settings_notifications_mode_title).lowercase().capitalize(Locale.current))
SectionViewSelectable(null, notificationsMode, modes, onNotificationsModeSelected) SectionViewSelectable(null, notificationsMode, modes, onNotificationsModeSelected)
} }
@@ -104,9 +100,7 @@ fun NotificationPreviewView(
onNotificationPreviewModeSelected: (NotificationPreviewMode) -> Unit, onNotificationPreviewModeSelected: (NotificationPreviewMode) -> Unit,
) { ) {
val previewModes = remember { notificationPreviewModes() } val previewModes = remember { notificationPreviewModes() }
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.settings_notification_preview_title)) AppBarTitle(stringResource(MR.strings.settings_notification_preview_title))
SectionViewSelectable(null, notificationPreviewMode, previewModes, onNotificationPreviewModeSelected) SectionViewSelectable(null, notificationPreviewMode, previewModes, onNotificationPreviewModeSelected)
} }
@@ -66,9 +66,7 @@ private fun PreferencesLayout(
reset: () -> Unit, reset: () -> Unit,
savePrefs: () -> Unit, savePrefs: () -> Unit,
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.your_preferences)) AppBarTitle(stringResource(MR.strings.your_preferences))
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.allow) } val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.allow) }
TimedMessagesFeatureSection(timedMessages) { TimedMessagesFeatureSection(timedMessages) {

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