Compare commits

..

10 Commits

Author SHA1 Message Date
IC Rainbow ace687b7d3 revert disable pcre2 jit 2024-10-28 15:28:51 +02:00
IC Rainbow 59b5d07252 add mingw64 paths to windows cabal.project.local 2024-10-28 13:34:07 +02:00
IC Rainbow 82400aac66 disable pcre2 jit 2024-10-28 13:27:30 +02:00
IC Rainbow 7df7434a92 fix mingw package name 2024-10-28 12:48:36 +02:00
IC Rainbow f243f01bd1 fix test build 2024-10-27 12:34:11 +02:00
IC Rainbow 6607c24de3 add missing fields 2024-10-27 12:23:22 +02:00
IC Rainbow 4086238090 fix package name 2024-10-27 12:23:14 +02:00
IC Rainbow a1bbecc7ad Merge remote-tracking branch 'origin/master' into ab/rocksdb 2024-10-27 12:19:35 +02:00
IC Rainbow efa0d6d551 add rocksdb to CI 2024-10-24 14:41:07 +03:00
IC Rainbow 17214a55e9 WIP: test rocksdb build 2024-10-24 13:48:56 +03:00
162 changed files with 2499 additions and 4588 deletions
+18 -1
View File
@@ -131,6 +131,10 @@ 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'
@@ -145,14 +149,22 @@ 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 run: brew install pkg-config rocksdb
- 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'
@@ -322,6 +334,7 @@ jobs:
git git
perl perl
make make
mingw-w64-x86_64-rocksdb
pacboy: >- pacboy: >-
toolchain:p toolchain:p
cmake:p cmake:p
@@ -341,6 +354,10 @@ 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 --constraint 'simplexmq +client_library' RUN cabal build exe:simplex-chat
# 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) && \
+13 -21
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-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.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 */; };
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-CTfGB7l09cqEHVIdvhrnH5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a */; }; 643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.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-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>"; }; 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>"; };
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-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>"; }; 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>"; };
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-CTfGB7l09cqEHVIdvhrnH5.a in Frameworks */, 643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a in Frameworks */,
643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a in Frameworks */, 643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-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-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a */, 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a */,
643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a */, 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */,
5CA059C2279559F40002BEB4 /* Shared */, 5CA059C2279559F40002BEB4 /* Shared */,
5CDCAD462818589900503DA2 /* SimpleX NSE */, 5CDCAD462818589900503DA2 /* SimpleX NSE */,
CEE723A82C3BD3D70009AE93 /* SimpleX SE */, CEE723A82C3BD3D70009AE93 /* SimpleX SE */,
@@ -912,9 +912,10 @@
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 */,
@@ -922,6 +923,9 @@
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 */,
@@ -1052,18 +1056,6 @@
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,10 +4,8 @@ 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
@@ -15,6 +13,7 @@ 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.*
@@ -25,21 +24,13 @@ 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.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight) platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
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.
@@ -56,7 +47,6 @@ class MainActivity: FragmentActivity() {
WindowManager.LayoutParams.FLAG_SECURE WindowManager.LayoutParams.FLAG_SECURE
) )
} }
enableEdgeToEdge()
setContent { setContent {
AppScreen() AppScreen()
} }
@@ -7,7 +7,6 @@ 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
@@ -17,7 +16,6 @@ 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
@@ -28,6 +26,7 @@ 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
@@ -275,32 +274,79 @@ class SimplexApp: Application(), LifecycleEventObserver {
uiModeManager.setApplicationNightMode(mode) uiModeManager.setApplicationNightMode(mode)
} }
override fun androidSetStatusAndNavigationBarAppearance(isLightStatusBar: Boolean, isLightNavBar: Boolean, blackNavBar: Boolean, themeBackgroundColor: Color) { override fun androidSetDrawerStatusAndNavBarColor(
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) {
windowInsetController?.isAppearanceLightStatusBars = statusLight var statusBar = (if (hasTop && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
} 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 {
OLD_ANDROID_UI_FLAGS
}
window.navigationBarColor = if (blackNavBar) Color.Black.toArgb() else themeBackgroundColor.toArgb()
} else { } else {
window.navigationBarColor = Color.Transparent.toArgb() if (CurrentColors.value.base == DefaultTheme.SIMPLEX) {
backgroundColor.lighter(0.4f)
} else {
backgroundColor
}
}).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
} }
} }
@@ -355,8 +401,6 @@ class SimplexApp: Application(), LifecycleEventObserver {
} }
return true return true
} }
override val androidApiLevel: Int get() = Build.VERSION.SDK_INT
} }
} }
} }
@@ -64,6 +64,7 @@ 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,13 +3,25 @@ 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: (File) -> Unit, onImage: (Painter) -> Unit,
onText: (String) -> Unit onText: (String) -> Unit
): Modifier = this ): Modifier = this
@@ -6,7 +6,8 @@ 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.* import android.view.OnReceiveContentListener
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
@@ -25,7 +26,6 @@ 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 = { context -> AndroidView(modifier = Modifier, factory = {
val editText = @SuppressLint("AppCompatCustomView") object: EditText(context) { val editText = @SuppressLint("AppCompatCustomView") object: EditText(it) {
override fun setOnReceiveContentListener( override fun setOnReceiveContentListener(
mimeTypes: Array<out String>?, mimeTypes: Array<out String>?,
listener: OnReceiveContentListener? listener: OnReceiveContentListener?
@@ -140,13 +140,6 @@ 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())
@@ -155,12 +148,8 @@ 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("") }
val workaround = WorkaroundFocusSearchLayout(context) editText
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.* import androidx.compose.ui.platform.LocalConfiguration
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,11 +50,7 @@ actual fun windowOrientation(): WindowOrientation = when (mainActivity.get()?.re
} }
@Composable @Composable
actual fun windowWidth(): Dp { actual fun windowWidth(): Dp = LocalConfiguration.current.screenWidthDp.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,12 +7,10 @@ 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.unit.Dp import androidx.compose.ui.platform.LocalDensity
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
@@ -27,74 +25,25 @@ 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 handler = LocalAppBarHandler.current val state = state ?: LocalAppBarHandler.current?.listState ?: rememberLazyListState()
require(handler != null) { "Using LazyColumnWithScrollBar and without AppBarHandler is an error. Use LazyColumnWithScrollBarNoAppBar instead" } val connection = LocalAppBarHandler.current?.connection
val state = state ?: handler.listState
val connection = handler.connection
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (reverseLayout) { snapshotFlow { state.firstVisibleItemScrollOffset }
snapshotFlow { state.layoutInfo.visibleItemsInfo.lastOrNull()?.offset ?: 0 } .filter { state.firstVisibleItemIndex == 0 }
.collect { scrollPosition -> .collect { scrollPosition ->
connection.appBarOffset = if (state.layoutInfo.visibleItemsInfo.lastOrNull()?.index == state.layoutInfo.totalItemsCount - 1) { val offset = connection?.appBarOffset
state.layoutInfo.viewportEndOffset - scrollPosition.toFloat() - state.layoutInfo.afterContentPadding if (offset != null && (offset + scrollPosition).absoluteValue > 1) {
} else { connection.appBarOffset = -scrollPosition.toFloat()
// show always when last item is not visible // Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
-1000f
}
//Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
} }
} else { }
snapshotFlow { state.firstVisibleItemScrollOffset }
.filter { state.firstVisibleItemIndex == 0 }
.collect { scrollPosition ->
val offset = connection.appBarOffset
if ((offset + scrollPosition + state.layoutInfo.afterContentPadding).absoluteValue > 1) {
connection.appBarOffset = -scrollPosition.toFloat()
//Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
}
}
}
} }
LazyColumn( if (connection != null) {
if (fillMaxSize) { LazyColumn(modifier.nestedScroll(connection), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
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()
} }
} }
@@ -105,80 +54,32 @@ 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 handler = LocalAppBarHandler.current val state = state ?: LocalAppBarHandler.current?.scrollState ?: rememberScrollState()
require(handler != null) { "Using ColumnWithScrollBar and without AppBarHandler is an error. Use ColumnWithScrollBarNoAppBar instead" } val connection = LocalAppBarHandler.current?.connection
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 + scrollPosition).absoluteValue > 1) { if (offset != null && (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}")
} }
} }
} }
val oneHandUI = remember { appPrefs.oneHandUI.state } if (connection != null) {
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 } else {
) { Column(if (maxIntrinsicSize) {
if (oneHandUI.value) {
Spacer(Modifier.padding(top = DEFAULT_PADDING + 5.dp).windowInsetsTopHeight(WindowInsets.statusBars))
content()
Spacer(Modifier.navigationBarsPadding().padding(bottom = AppBarHeight * fontSizeSqrtMultiplier))
} else {
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 }, verticalArrangement, horizontalAlignment, content)
) {
if (oneHandUI.value) {
Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars))
content()
} else {
content()
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
}
}
if (!oneHandUI.value) {
NavigationBarBackground(false, false)
}
} }
} }
@@ -3,18 +3,17 @@ 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
@@ -44,13 +43,28 @@ actual fun LocalMultiplatformView(): Any? = LocalView.current
@Composable @Composable
actual fun getKeyboardState(): State<KeyboardState> { actual fun getKeyboardState(): State<KeyboardState> {
val density = LocalDensity.current val keyboardState = remember { mutableStateOf(KeyboardState.Closed) }
val ime = WindowInsets.ime val view = LocalView.current
return remember { DisposableEffect(view) {
derivedStateOf { val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
if (ime.getBottom(density) == 0) KeyboardState.Closed else KeyboardState.Opened val rect = Rect()
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,6 +22,7 @@ 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
@@ -47,7 +48,6 @@ 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,14 +329,11 @@ private fun ActiveCallOverlayLayout(
flipCamera: () -> Unit flipCamera: () -> Unit
) { ) {
Column { Column {
CallAppBar( CloseSheetBar({ chatModel.activeCallViewIsCollapsed.value = true }, true, tintColor = Color(0xFFFFFFD8)) {
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) {
@@ -414,7 +411,6 @@ 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)
@@ -423,7 +419,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 = ripple, indication = rememberRipple(bounded = false, radius = size / 2, color = background.lighter(0.1f)),
enabled = enabled enabled = enabled
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -594,9 +590,8 @@ fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Uni
} }
} }
} else { } else {
ModalView(background = Color.Black, showAppBar = false, close = {}) { ModalView(background = Color.Black, showClose = false, close = {}) {
Column { ColumnWithScrollBar(Modifier.fillMaxSize()) {
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,6 +8,7 @@ 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
@@ -38,6 +39,14 @@ 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,26 +1,32 @@
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.* import androidx.compose.ui.unit.dp
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
@@ -32,12 +38,11 @@ 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) }
val statusBar = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT)) {
Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT + statusBar)) {
val source = remember { MutableInteractionSource() } val source = remember { MutableInteractionSource() }
val ripple = remember { ripple(bounded = true, 3000.dp) } val indication = rememberRipple(bounded = true, 3000.dp)
Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT + statusBar).clickable(onClick = onClick, indication = ripple, interactionSource = source)) { Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT).clickable(onClick = onClick, indication = indication, interactionSource = source)) {
GreenLine(statusBar, call) GreenLine(call)
} }
Box( Box(
Modifier Modifier
@@ -45,7 +50,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 = ripple, interactionSource = source) .clickable(onClick = onClick, indication = indication, interactionSource = source)
.align(Alignment.BottomCenter), .align(Alignment.BottomCenter),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
@@ -58,13 +63,16 @@ 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(statusBarHeight: Dp, call: Call) { private fun GreenLine(call: Call) {
Row( Row(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.background(SimplexGreen) .background(SimplexGreen)
.padding(top = -CALL_TOP_OFFSET + statusBarHeight) .padding(top = -CALL_TOP_OFFSET)
.padding(horizontal = DEFAULT_PADDING), .padding(horizontal = DEFAULT_PADDING),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
@@ -73,10 +81,12 @@ private fun GreenLine(statusBarHeight: Dp, 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) {
platform.androidSetStatusAndNavigationBarAppearance(false, CurrentColors.value.colors.isLight) statusBarColorAfterCall.intValue = window.statusBarColor
window.statusBarColor = SimplexGreen.toArgb()
onDispose { onDispose {
platform.androidSetStatusAndNavigationBarAppearance(CurrentColors.value.colors.isLight, CurrentColors.value.colors.isLight) window.statusBarColor = statusBarColorAfterCall.intValue
} }
} }
} }
@@ -19,11 +19,13 @@ 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
@@ -34,7 +36,6 @@ 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,
) { ) {
@@ -139,73 +140,87 @@ actual fun PlatformUserPicker(modifier: Modifier, pickerState: MutableStateFlow<
} else { } else {
Modifier Modifier
} }
Box { Box(
Modifier
.fillMaxSize()
.then(clickableModifier)
.drawBehind {
val pos = 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
}
val colors = CurrentColors.value.colors
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(
if (pos != 0f) resultingColor else Color.Transparent,
alpha = calculateFraction(pos = pos)
)
}
.graphicsLayer {
if (heightValue == 0) {
alpha = 0f
}
translationY = dismissState.offset.value
},
contentAlignment = Alignment.BottomCenter
) {
Box( Box(
Modifier Modifier.onSizeChanged { height.intValue = it.height }
.fillMaxSize()
.then(clickableModifier)
.drawBehind {
val pos = calculatePosition(dismissState)
val colors = CurrentColors.value.colors
val resultingColor = if (colors.isLight) colors.onSurface.copy(alpha = ScrimOpacity) else Color.Black.copy(0.64f)
drawRect(
if (pos != 0f) resultingColor else Color.Transparent,
alpha = calculateFraction(pos = pos)
)
}
.graphicsLayer {
if (heightValue == 0) {
alpha = 0f
}
translationY = dismissState.offset.value
},
contentAlignment = Alignment.BottomCenter
) { ) {
Box( KeyChangeEffect(pickerIsVisible) {
Modifier.onSizeChanged { height.intValue = it.height } if (pickerState.value.isVisible()) {
) { try {
KeyChangeEffect(pickerIsVisible) { dismissState.animateTo(DismissValue.Default, userPickerAnimSpec())
if (pickerState.value.isVisible()) { } catch (e: CancellationException) {
try { Log.e(TAG, "Cancelled animateTo: ${e.stackTraceToString()}")
dismissState.animateTo(DismissValue.Default, userPickerAnimSpec()) pickerState.value = AnimatedViewState.GONE
} catch (e: CancellationException) { }
Log.e(TAG, "Cancelled animateTo: ${e.stackTraceToString()}") } else {
pickerState.value = AnimatedViewState.GONE try {
} dismissState.animateTo(DismissValue.DismissedToEnd, userPickerAnimSpec())
} else { } catch (e: CancellationException) {
try { Log.e(TAG, "Cancelled animateTo2: ${e.stackTraceToString()}")
dismissState.animateTo(DismissValue.DismissedToEnd, userPickerAnimSpec()) pickerState.value = AnimatedViewState.VISIBLE
} catch (e: CancellationException) {
Log.e(TAG, "Cancelled animateTo2: ${e.stackTraceToString()}")
pickerState.value = AnimatedViewState.VISIBLE
}
} }
}
val draggableModifier = if (height.intValue != 0)
Modifier.draggableBottomDrawerModifier(
state = dismissState,
swipeDistance = height.intValue.toFloat(),
)
else Modifier
Box(draggableModifier.then(modifier).navigationBarsPadding()) {
content()
} }
} }
val draggableModifier = if (height.intValue != 0)
Modifier.draggableBottomDrawerModifier(
state = dismissState,
swipeDistance = height.intValue.toFloat(),
)
else Modifier
Box(draggableModifier.then(modifier)) {
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(
state: DismissState, state: DismissState,
swipeDistance: Float, swipeDistance: Float,
@@ -171,8 +171,6 @@ 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()
} }
@@ -1,41 +0,0 @@
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,7 +2,6 @@ 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
@@ -32,7 +31,6 @@ import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.helpers.APPLICATION_ID import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.common.helpers.saveAppLocale import chat.simplex.common.helpers.saveAppLocale
import chat.simplex.common.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
@@ -77,7 +75,9 @@ 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,13 +13,14 @@ 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(withAuth) }) SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) })
AppVersionItem(showVersion) AppVersionItem(showVersion)
} }
} }
@@ -11,13 +11,10 @@ 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.* import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color
import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.graphicsLayer
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.*
@@ -42,39 +39,14 @@ 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 {
Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) { ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
// This padding applies to landscape view only taking care of navigation bar and holes in screen in status bar area Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
// (because nav bar and holes located on vertical sides of screen in landscape view) MainScreen()
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()
}
}
} }
} }
} }
@@ -166,9 +138,7 @@ fun MainScreen() {
} }
SetupClipboardListener() SetupClipboardListener()
if (appPlatform.isAndroid) { if (appPlatform.isAndroid) {
AndroidWrapInCallLayout { AndroidScreen(userPickerState)
AndroidScreen(userPickerState)
}
} else { } else {
DesktopScreen(userPickerState) DesktopScreen(userPickerState)
} }
@@ -200,9 +170,7 @@ fun MainScreen() {
} }
} }
if (appPlatform.isAndroid) { if (appPlatform.isAndroid) {
AndroidWrapInCallLayout { ModalManager.fullscreen.showInView()
ModalManager.fullscreen.showInView()
}
SwitchingUsersView() SwitchingUsersView()
} }
@@ -269,39 +237,19 @@ fun MainScreen() {
val ANDROID_CALL_TOP_PADDING = 40.dp val ANDROID_CALL_TOP_PADDING = 40.dp
@Composable
fun AndroidWrapInCallLayout(content: @Composable () -> Unit) {
val call = remember { chatModel.activeCall}.value
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 @Composable
fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) { fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
BoxWithConstraints { BoxWithConstraints {
val call = remember { chatModel.activeCall} .value
val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted
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 {
// minOf thing is needed for devices with holes in screen while the user on ChatView rotates his phone from portrait to landscape translationX = -offset.value.dp.toPx()
// 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)
} }
@@ -323,40 +271,51 @@ fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
snapshotFlow { chatModel.chatId.value } snapshotFlow { chatModel.chatId.value }
.distinctUntilChanged() .distinctUntilChanged()
.collect { .collect {
if (it == null) onComposed(null) if (it == 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
.then(if (hasCutout) Modifier.clip(RectangleShape) else Modifier) .graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }
.graphicsLayer { translationX = maxWidth.toPx() - minOf(offset.value.dp, maxWidth).toPx() } .padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)
) 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
} ShareListView(chatModel, stopped)
} else {
// LALAL initial load of view doesn't show blur. Focusing text field shows it
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler(keyboardCoversBar = false)) {
ShareListView(chatModel, stopped)
}
}
} }
} }
@@ -90,9 +90,6 @@ 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,9 +118,6 @@ 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)
@@ -226,8 +223,6 @@ 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)
@@ -249,7 +244,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, true) val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, appPlatform.isAndroid)
val hintPreferences: List<Pair<SharedPreference<Boolean>, Boolean>> = listOf( val hintPreferences: List<Pair<SharedPreference<Boolean>, Boolean>> = listOf(
laNoticeShown to false, laNoticeShown to false,
@@ -367,7 +362,6 @@ 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"
@@ -434,7 +428,6 @@ 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,11 +14,20 @@ 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: (File) -> Unit = {}, onImage: (Painter) -> Unit = {},
onText: (String) -> Unit = {} onText: (String) -> Unit = {}
): Modifier ): Modifier
@@ -8,7 +8,6 @@ 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 {
@@ -21,12 +20,12 @@ interface PlatformInterface {
fun androidChatInitializedAndStarted() {} fun androidChatInitializedAndStarted() {}
fun androidIsBackgroundCallAllowed(): Boolean = true fun androidIsBackgroundCallAllowed(): Boolean = true
fun androidSetNightModeIfSupported() {} fun androidSetNightModeIfSupported() {}
fun androidSetStatusAndNavigationBarAppearance(isLightStatusBar: Boolean, isLightNavBar: Boolean, blackNavBar: Boolean = false, themeBackgroundColor: Color = CurrentColors.value.colors.background) {} fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {}
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,7 +8,6 @@ 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
@@ -22,44 +21,11 @@ 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.* import androidx.compose.ui.unit.Density
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,27 +587,21 @@ data class ThemeModeOverride (
} }
} }
fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, bgLayerSize: MutableState<IntSize>?, bgLayer: GraphicsLayer?/*, shape: Shape = RectangleShape*/): Modifier { fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, shape: Shape = RectangleShape): Modifier {
return drawBehind { return if (baseTheme == DefaultTheme.SIMPLEX) {
copyBackgroundToAppBar(bgLayerSize, bgLayer) { this.background(brush = Brush.linearGradient(
if (baseTheme == DefaultTheme.SIMPLEX) { listOf(
drawRect(brush = themedBackgroundBrush()) CurrentColors.value.colors.background.darker(0.4f),
} else { CurrentColors.value.colors.background.lighter(0.4f)
drawRect(CurrentColors.value.colors.background) ),
} Offset(0f, Float.POSITIVE_INFINITY),
} Offset(Float.POSITIVE_INFINITY, 0f)
), shape = shape)
} else {
this.background(color = CurrentColors.value.colors.background, shape = shape)
} }
} }
fun themedBackgroundBrush(): Brush = Brush.linearGradient(
listOf(
CurrentColors.value.colors.background.darker(0.4f),
CurrentColors.value.colors.background.lighter(0.4f)
),
Offset(0f, Float.POSITIVE_INFINITY),
Offset(Float.POSITIVE_INFINITY, 0f)
)
val DEFAULT_PADDING = 20.dp val DEFAULT_PADDING = 20.dp
val DEFAULT_SPACE_AFTER_ICON = 4.dp val DEFAULT_SPACE_AFTER_ICON = 4.dp
val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2 val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2
@@ -1,6 +1,7 @@
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
@@ -106,7 +107,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.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight) platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !ChatController.appPrefs.oneHandUI.get(), ChatController.appPrefs.oneHandUI.get())
} }
fun changeDarkTheme(theme: String) { fun changeDarkTheme(theme: String) {
@@ -124,6 +125,10 @@ 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,34 +7,40 @@ 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.* import androidx.compose.ui.unit.dp
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 chat.simplex.common.views.chat.item.CONSOLE_COMPOSE_LAYOUT_ID import kotlinx.coroutines.flow.collect
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) { fun TerminalView(floating: Boolean = false, close: () -> Unit) {
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
) )
} }
@@ -63,6 +69,7 @@ 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) }
@@ -70,63 +77,65 @@ 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 }
Box(Modifier.fillMaxSize()) { ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
val composeViewHeight = remember { mutableStateOf(0.dp) } Scaffold(
AdaptingBottomPaddingLayout(Modifier, CONSOLE_COMPOSE_LAYOUT_ID, composeViewHeight) { topBar = { CloseSheetBar(close) },
TerminalLog(floating, composeViewHeight) bottomBar = {
Column( Column {
Modifier Divider()
.layoutId(CONSOLE_COMPOSE_LAYOUT_ID) Box(Modifier.padding(horizontal = 8.dp)) {
.align(Alignment.BottomCenter) SendMsgView(
.navigationBarsPadding() composeState = composeState,
.consumeWindowInsets(PaddingValues(bottom = if (oneHandUI.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp)) showVoiceRecordIcon = false,
.imePadding() recState = remember { mutableStateOf(RecordingState.NotStarted) },
.padding(bottom = if (oneHandUI.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp) isDirectChat = false,
.background(MaterialTheme.colors.background) liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
) { sendMsgEnabled = true,
Divider() sendButtonEnabled = true,
Box(Modifier.padding(horizontal = 8.dp)) { nextSendGrpInv = false,
SendMsgView( needToAllowVoiceToContact = false,
composeState = composeState, allowedVoiceByPrefs = false,
showVoiceRecordIcon = false, userIsObserver = false,
recState = remember { mutableStateOf(RecordingState.NotStarted) }, userCanSend = true,
isDirectChat = false, allowVoiceToContact = {},
liveMessageAlertShown = SharedPreference(get = { false }, set = {}), placeholder = "",
sendMsgEnabled = true, sendMessage = { sendCommand() },
sendButtonEnabled = true, sendLiveMessage = null,
nextSendGrpInv = false, updateLiveMessage = null,
needToAllowVoiceToContact = false, editPrevMessage = {},
allowedVoiceByPrefs = false, onMessageChange = ::onMessageChange,
userIsObserver = false, onFilesPasted = {},
userCanSend = true, textStyle = textStyle
allowVoiceToContact = {}, )
placeholder = "", }
sendMessage = { sendCommand() },
sendLiveMessage = null,
updateLiveMessage = null,
editPrevMessage = {},
onMessageChange = ::onMessageChange,
onFilesPasted = {},
textStyle = textStyle
)
} }
},
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, composeViewHeight: State<Dp>) { fun TerminalLog(floating: Boolean) {
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 = listState.firstVisibleItemIndex <= 1 var autoScrollToBottom = true
launch { launch {
snapshotFlow { listState.layoutInfo.totalItemsCount } snapshotFlow { listState.layoutInfo.totalItemsCount }
.filter { autoScrollToBottom } .filter { autoScrollToBottom }
@@ -141,21 +150,12 @@ fun TerminalLog(floating: Boolean, composeViewHeight: State<Dp>) {
launch { launch {
snapshotFlow { listState.firstVisibleItemIndex } snapshotFlow { listState.firstVisibleItemIndex }
.collect { .collect {
autoScrollToBottom = it == 0 autoScrollToBottom = listState.firstVisibleItemIndex == 0
} }
} }
} }
LazyColumnWithScrollBar ( LazyColumnWithScrollBar(reverseLayout = true, state = listState) {
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,15 +172,13 @@ fun TerminalLog(floating: Boolean, composeViewHeight: State<Dp>) {
ModalManager.start ModalManager.start
} }
modalPlace.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) { modalPlace.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) {
ColumnWithScrollBar { SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
SelectionContainer { val details = item.details
val details = item.details .let {
.let { if (it.length < 100_000) it
if (it.length < 100_000) it else it.substring(0, 100_000)
else it.substring(0, 100_000) }
} 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)
@@ -210,7 +208,8 @@ fun PreviewTerminalLayout() {
TerminalLayout( TerminalLayout(
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) },
sendCommand = {}, sendCommand = {},
floating = false floating = false,
close = {}
) )
} }
} }
@@ -40,6 +40,8 @@ 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()
@@ -48,9 +50,11 @@ 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), withPadding = false, bottomPadding = DEFAULT_PADDING) AppBarTitle(stringResource(MR.strings.create_profile), 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),
@@ -98,6 +102,7 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
} }
} }
} }
}
} }
@Composable @Composable
@@ -106,42 +111,59 @@ 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) }
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { val handler = remember { AppBarHandler() }
ModalView({ CompositionLocalProvider(
if (chatModel.users.none { !it.user.hidden }) { LocalAppBarHandler provides handler
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) ) {
} else { ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
close() Column(
} modifier = Modifier
}) { .fillMaxSize()
ColumnWithScrollBar { .themedBackground(),
val displayName = rememberSaveable { mutableStateOf("") } horizontalAlignment = Alignment.CenterHorizontally
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)) { CloseSheetBar(close = {
Box(Modifier.align(Alignment.CenterHorizontally)) { if (chatModel.users.none { !it.user.hidden }) {
AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING, withPadding = false) appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
} else {
close()
} }
ProfileNameField(displayName, stringResource(MR.strings.display_name), { it.trim() == mkValidName(it) }, focusRequester) })
Spacer(Modifier.height(DEFAULT_PADDING)) BackHandler(onBack = {
ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Start, padding = PaddingValues(), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Start, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) })
}
Spacer(Modifier.fillMaxHeight().weight(1f))
Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) {
OnboardingActionButton(
if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
labelId = MR.strings.create_profile_button,
onboarding = null,
enabled = canCreateProfile(displayName.value),
onclick = { createProfileOnboarding(chat.simplex.common.platform.chatModel, displayName.value, close) }
)
// Reserve space
TextButtonBelowOnboardingButton("", null)
}
LaunchedEffect(Unit) { ColumnWithScrollBar(
delay(300) modifier = Modifier.fillMaxSize()
focusRequester.requestFocus() ) {
val displayName = rememberSaveable { mutableStateOf("") }
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)) {
Box(Modifier.align(Alignment.CenterHorizontally)) {
AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING, withPadding = false)
}
ProfileNameField(displayName, stringResource(MR.strings.display_name), { it.trim() == mkValidName(it) }, focusRequester)
Spacer(Modifier.height(DEFAULT_PADDING))
ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Start, padding = PaddingValues(), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Start, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
}
Spacer(Modifier.fillMaxHeight().weight(1f))
Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) {
OnboardingActionButton(
if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
labelId = MR.strings.create_profile_button,
onboarding = null,
enabled = canCreateProfile(displayName.value),
onclick = { createProfileOnboarding(chat.simplex.common.platform.chatModel, displayName.value, close) }
)
// Reserve space
TextButtonBelowOnboardingButton("", null)
}
LaunchedEffect(Unit) {
delay(300)
focusRequester.requestFocus()
}
} }
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -233,6 +255,7 @@ 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
@@ -266,7 +289,6 @@ 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).statusBarsPadding().padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) { Column(Modifier.fillMaxWidth().background(color).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,7 +529,10 @@ 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 { ColumnWithScrollBar(Modifier.fillMaxWidth()) {
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 { ColumnWithScrollBar(Modifier.fillMaxWidth()) {
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 { ColumnWithScrollBar(Modifier.fillMaxWidth()) {
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 { ColumnWithScrollBar(Modifier.fillMaxWidth()) {
Details() Details()
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
val mss = membersStatuses(chatModel, memberDeliveryStatuses) val mss = membersStatuses(chatModel, memberDeliveryStatuses)
@@ -12,11 +12,10 @@ 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.* import androidx.compose.ui.draw.clip
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
@@ -109,7 +108,6 @@ 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 }
@@ -525,10 +523,28 @@ 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)
}) })
@@ -537,9 +553,14 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
ModalManager.end.closeModals() ModalManager.end.closeModals()
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)
}) })
@@ -548,10 +569,10 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
ModalManager.end.closeModals() ModalManager.end.closeModals()
chatModel.chatItems.clear() chatModel.chatItems.clear()
} }
}
} }
else -> {} else -> {}
} }
}
} }
} }
@@ -621,67 +642,81 @@ 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 = { file -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(file.toURI()), null) } }, onImage = {
// 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)
}, },
) )
) { ) {
ModalBottomSheetLayout( ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
scrimColor = Color.Black.copy(alpha = 0.12F), ModalBottomSheetLayout(
sheetElevation = 0.dp, scrimColor = Color.Black.copy(alpha = 0.12F),
sheetContent = { modifier = Modifier.navigationBarsWithImePadding(),
ChooseAttachmentView( sheetElevation = 0.dp,
attachmentOption, sheetContent = {
hide = { scope.launch { attachmentBottomSheetState.hide() } } ChooseAttachmentView(
) attachmentOption,
}, hide = { scope.launch { attachmentBottomSheetState.hide() } }
sheetState = attachmentBottomSheetState, )
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) },
) { sheetState = attachmentBottomSheetState,
val composeViewHeight = remember { mutableStateOf(0.dp) } sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer)) { ) {
val remoteHostId = remember { remoteHostId }.value val floatingButton: MutableState<@Composable () -> Unit> = remember { mutableStateOf({}) }
val chatInfo = remember { chatInfo }.value val setFloatingButton = { button: @Composable () -> Unit ->
AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) { floatingButton.value = button
if (chatInfo != null) { }
Box(Modifier.fillMaxSize()) {
Scaffold(
topBar = {
if (selectedChatItems.value == null) {
val chatInfo = chatInfo.value
if (chatInfo != null) {
ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
}
} else {
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( ChatItemsList(
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue, remoteHostId, chatInfo, unreadCount, composeState, searchValue,
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages, useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markRead, remember { { onComposed(it) } }, developerTools, showViaProxy, setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy,
) )
} }
} }
val oneHandUI = remember { appPrefs.oneHandUI.state }
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 (chatInfo != null) {
ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
}
} else {
SelectedItemsTopToolbar(selectedChatItems)
}
} }
} }
} }
@@ -689,7 +724,7 @@ fun ChatLayout(
} }
@Composable @Composable
fun BoxScope.ChatInfoToolbar( fun ChatInfoToolbar(
chatInfo: ChatInfo, chatInfo: ChatInfo,
back: () -> Unit, back: () -> Unit,
info: () -> Unit, info: () -> Unit,
@@ -841,33 +876,21 @@ fun BoxScope.ChatInfoToolbar(
} }
} }
} }
val oneHandUI = remember { appPrefs.oneHandUI.state }
DefaultAppBar( DefaultTopAppBar(
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.forEach { it() } } buttons = barButtons
) )
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) {
val density = LocalDensity.current Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier))
val width = remember { mutableStateOf(250.dp) }
val height = remember { mutableStateOf(0.dp) } Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight * fontSizeSqrtMultiplier)) {
DefaultDropdownMenu( DefaultDropdownMenu(showMenu) {
showMenu, menuItems.forEach { it() }
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() }
}
} }
} }
} }
@@ -911,12 +934,11 @@ private fun ContactVerifiedShield() {
} }
@Composable @Composable
fun BoxScope.ChatItemsList( fun BoxWithConstraintsScope.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,
@@ -941,6 +963,7 @@ fun BoxScope.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
@@ -964,18 +987,13 @@ fun BoxScope.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 = remember { derivedStateOf { chatModel.chatItems.asReversed() } } val reversedChatItems by remember { derivedStateOf { chatModel.chatItems.asReversed() } }
val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent().roundToPx() }) val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() }
val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } } val scrollToItem: (Long) -> Unit = { itemId: Long ->
val scrollToItem: State<(Long) -> Unit> = remember { val index = reversedChatItems.indexOfFirst { it.id == itemId }
mutableStateOf( if (index != -1) {
{ itemId: Long -> scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.lastIndex, index + 1), -maxHeightRounded) }
val index = reversedChatItems.value.indexOfFirst { it.id == itemId } }
if (index != -1) {
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) {
@@ -993,18 +1011,8 @@ fun BoxScope.ChatItemsList(
VideoPlayerHolder.releaseAll() VideoPlayerHolder.releaseAll()
} }
) )
LazyColumnWithScrollBar( LazyColumnWithScrollBar(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
Modifier.align(Alignment.BottomCenter), itemsIndexed(reversedChatItems, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem ->
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
@@ -1012,10 +1020,10 @@ fun BoxScope.ChatItemsList(
) { ) {
val provider = { val provider = {
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed -> providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
itemScope.launch { scope.launch {
listState.scrollToItem( listState.scrollToItem(
kotlin.math.min(reversedChatItems.value.lastIndex, indexInReversed + 1), kotlin.math.min(reversedChatItems.lastIndex, indexInReversed + 1),
-maxHeight.value -maxHeightRounded
) )
} }
} }
@@ -1028,7 +1036,7 @@ fun BoxScope.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.value, 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, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
} }
} }
@@ -1036,7 +1044,7 @@ fun BoxScope.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) {
itemScope.launch { scope.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)
@@ -1233,17 +1241,16 @@ fun BoxScope.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) {
reversed.subList(range.first, range.last + 1).forEachIndexed { index, ci -> reversedChatItems.subList(range.first, range.last + 1).forEachIndexed { index, ci ->
val prev = if (index + range.first == prevHidden) prevItem else reversed[index + range.first + 1] val prev = if (index + range.first == prevHidden) prevItem else reversedChatItems[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 == reversed.lastIndex) { if (i == reversedChatItems.lastIndex) {
DateSeparator(cItem.meta.itemTs) DateSeparator(cItem.meta.itemTs)
} }
} }
@@ -1251,7 +1258,7 @@ fun BoxScope.ChatItemsList(
if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) { if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) {
LaunchedEffect(cItem.id) { LaunchedEffect(cItem.id) {
itemScope.launch { scope.launch {
delay(600) delay(600)
markRead(CC.ItemRange(cItem.id, cItem.id), null) markRead(CC.ItemRange(cItem.id, cItem.id), null)
} }
@@ -1260,10 +1267,10 @@ fun BoxScope.ChatItemsList(
} }
} }
} }
FloatingButtons(chatModel.chatItems, unreadCount, composeViewHeight, remoteHostId, chatInfo, searchValue, markRead, listState) FloatingButtons(chatModel.chatItems, unreadCount, remoteHostId, chatInfo, searchValue, markRead, setFloatingButton, listState)
FloatingDate( FloatingDate(
Modifier.padding(top = 10.dp + topPaddingToContent()).align(Alignment.TopCenter), Modifier.padding(top = 10.dp).align(Alignment.TopCenter),
listState, listState,
) )
@@ -1318,65 +1325,87 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
} }
@Composable @Composable
fun BoxScope.FloatingButtons( fun BoxWithConstraintsScope.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()
val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportSize.height } } var firstVisibleIndex by remember { mutableStateOf(listState.firstVisibleItemIndex) }
val bottomUnreadCount = remember { var lastIndexOfVisibleItems by remember { mutableStateOf(listState.layoutInfo.visibleItemsInfo.lastIndex) }
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 - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex val from = items.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems
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()
val showBottomButtonWithCounter = remember { derivedStateOf { bottomUnreadCount.value > 0 && listState.firstVisibleItemIndex != 0 && searchValue.value.isEmpty() } } LaunchedEffect(bottomUnreadCount, firstItemIsVisible) {
val showBottomButtonWithArrow = remember { derivedStateOf { !showBottomButtonWithCounter.value && listState.firstVisibleItemIndex != 0 } } val showButtonWithCounter = bottomUnreadCount > 0 && !firstItemIsVisible && searchValue.value.isEmpty()
BottomEndFloatingButton( val showButtonWithArrow = !showButtonWithCounter && !firstItemIsVisible
bottomUnreadCount, setFloatingButton(
showBottomButtonWithCounter, bottomEndFloatingButton(
showBottomButtonWithArrow, bottomUnreadCount,
composeViewHeight, showButtonWithCounter,
onClickArrowDown = { showButtonWithArrow,
scope.launch { listState.animateScrollToItem(0) } onClickArrowDown = {
}, scope.launch { listState.animateScrollToItem(0) }
onClickCounter = { },
val firstVisibleOffset = (-maxHeight.value * 0.8).toInt() onClickCounter = {
scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount.value - 1), firstVisibleOffset) } scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount - 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 = remember { derivedStateOf { unreadCount.value - bottomUnreadCount.value } } val topUnreadCount by remember {
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 + topPaddingToContent()).align(Alignment.TopEnd), Modifier.padding(end = DEFAULT_PADDING, top = 24.dp).align(Alignment.TopEnd),
topUnreadCount, topUnreadCount,
onClick = { scope.launch { listState.animateScrollBy(maxHeight.value.toFloat()) } }, showButtonWithCounter,
onClick = { scope.launch { listState.animateScrollBy(height) } },
onLongClick = { showDropDown.value = true } onLongClick = { showDropDown.value = true }
) )
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) { Box {
val density = LocalDensity.current DefaultDropdownMenu(showDropDown, offset = DpOffset(this@FloatingButtons.maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) {
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),
@@ -1384,7 +1413,7 @@ fun BoxScope.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.value bottomUnreadCount
) )
showDropDown.value = false showDropDown.value = false
}) })
@@ -1446,11 +1475,12 @@ fun MemberImage(member: GroupMember) {
@Composable @Composable
private fun TopEndFloatingButton( private fun TopEndFloatingButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
unreadCount: State<Int>, unreadCount: Int,
showButtonWithCounter: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit onLongClick: () -> Unit
) = when { ) = when {
unreadCount.value > 0 -> { showButtonWithCounter -> {
val interactionSource = interactionSourceWithDetection(onClick, onLongClick) val interactionSource = interactionSourceWithDetection(onClick, onLongClick)
FloatingActionButton( FloatingActionButton(
{}, // no action here {}, // no action here
@@ -1460,7 +1490,7 @@ private fun TopEndFloatingButton(
interactionSource = interactionSource, interactionSource = interactionSource,
) { ) {
Text( Text(
unreadCountStr(unreadCount.value), unreadCountStr(unreadCount),
color = MaterialTheme.colors.primary, color = MaterialTheme.colors.primary,
fontSize = 14.sp, fontSize = 14.sp,
) )
@@ -1470,16 +1500,6 @@ 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,
@@ -1489,9 +1509,8 @@ 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) { if (listState.layoutInfo.visibleItemsInfo.lastIndex >= 0 && listState.firstVisibleItemIndex >= 0) {
val lastFullyVisibleOffset = listState.layoutInfo.viewportEndOffset val lastVisibleChatItemIndex = chatModel.chatItems.value.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex
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)
@@ -1678,44 +1697,48 @@ fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (
} }
} }
@Composable private fun bottomEndFloatingButton(
private fun BoxScope.BottomEndFloatingButton( unreadCount: Int,
unreadCount: State<Int>, showButtonWithCounter: Boolean,
showButtonWithCounter: State<Boolean>, showButtonWithArrow: Boolean,
showButtonWithArrow: State<Boolean>,
composeViewHeight: State<Dp>,
onClickArrowDown: () -> Unit, onClickArrowDown: () -> Unit,
onClickCounter: () -> Unit onClickCounter: () -> Unit
) = when { ): @Composable () -> Unit = when {
showButtonWithCounter.value -> { showButtonWithCounter -> {
FloatingActionButton( {
onClick = onClickCounter, FloatingActionButton(
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), onClick = onClickCounter,
modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp), elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
backgroundColor = MaterialTheme.colors.secondaryVariant, modifier = Modifier.size(48.dp),
) { backgroundColor = MaterialTheme.colors.secondaryVariant,
Text( ) {
unreadCountStr(unreadCount.value), Text(
color = MaterialTheme.colors.primary, unreadCountStr(unreadCount),
fontSize = 14.sp, color = MaterialTheme.colors.primary,
) fontSize = 14.sp,
)
}
} }
} }
showButtonWithArrow.value -> { showButtonWithArrow -> {
FloatingActionButton( {
onClick = onClickArrowDown, FloatingActionButton(
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), onClick = onClickArrowDown,
modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp), elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
backgroundColor = MaterialTheme.colors.secondaryVariant, modifier = Modifier.size(48.dp),
) { backgroundColor = MaterialTheme.colors.secondaryVariant,
Icon( ) {
painter = painterResource(MR.images.ic_keyboard_arrow_down), Icon(
contentDescription = null, painter = painterResource(MR.images.ic_keyboard_arrow_down),
tint = MaterialTheme.colors.primary contentDescription = null,
) tint = MaterialTheme.colors.primary
)
}
} }
} }
else -> {} else -> {
{}
}
} }
@Composable @Composable
@@ -1842,25 +1865,6 @@ 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,14 +13,12 @@ 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
@@ -898,7 +896,7 @@ fun ComposeView(
} }
} }
} }
Box(Modifier.background(MaterialTheme.colors.background)) { Column(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)
@@ -920,7 +918,7 @@ fun ComposeView(
&& !nextSendGrpInv.value && !nextSendGrpInv.value
IconButton( IconButton(
attachmentClicked, attachmentClicked,
Modifier.padding(start = 3.dp, end = 1.dp, bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier), Modifier.padding(bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier),
enabled = attachmentEnabled enabled = attachmentEnabled
) { ) {
Icon( Icon(
@@ -81,7 +81,10 @@ 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,11 +1,9 @@
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
@@ -14,7 +12,9 @@ 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) {
ColumnWithScrollBar { Column(
Modifier.fillMaxSize()
) {
AppBarTitle(stringResource(MR.strings.scan_code)) AppBarTitle(stringResource(MR.strings.scan_code))
QRCodeScanner { text -> QRCodeScanner { text ->
verifyCode(text) { verifyCode(text) {
@@ -28,6 +28,5 @@ 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,7 +12,6 @@ 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.*
@@ -21,12 +20,11 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
@Composable @Composable
fun BoxScope.SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>) { fun 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
val oneHandUI = remember { appPrefs.oneHandUI.state } DefaultTopAppBar(
DefaultAppBar(
navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) }, navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) },
title = { title = {
Text( Text(
@@ -41,9 +39,10 @@ fun BoxScope.SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>
) )
}, },
onTitleClick = null, onTitleClick = null,
onTop = !oneHandUI.value, showSearch = false,
onSearchValueChanged = {}, onSearchValueChanged = {},
) )
Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier))
} }
@Composable @Composable
@@ -69,8 +68,6 @@ 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
@@ -106,7 +103,6 @@ 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,6 +7,7 @@ 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.*
@@ -14,7 +15,6 @@ 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,8 +61,7 @@ fun SendMsgView(
) { ) {
val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) } val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) }
val padding = if (appPlatform.isAndroid) PaddingValues(vertical = 8.dp) else PaddingValues(top = 3.dp, bottom = 4.dp) Box(Modifier.padding(vertical = if (appPlatform.isAndroid) 8.dp else 6.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) {
@@ -148,7 +147,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(12.dp)) Spacer(Modifier.width(10.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)
@@ -424,7 +423,6 @@ 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(
@@ -433,7 +431,7 @@ private fun SendMsgButton(
enabled = enabled, enabled = enabled,
role = Role.Button, role = Role.Button,
interactionSource = interactionSource, interactionSource = interactionSource,
indication = ripple indication = rememberRipple(bounded = false, radius = 24.dp)
) )
.onRightClick { onLongClick?.invoke() }, .onRightClick { onLongClick?.invoke() },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -456,7 +454,6 @@ 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(
@@ -464,7 +461,7 @@ private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
enabled = enabled, enabled = enabled,
role = Role.Button, role = Role.Button,
interactionSource = interactionSource, interactionSource = interactionSource,
indication = ripple indication = rememberRipple(bounded = false, radius = 24.dp)
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
@@ -56,7 +56,11 @@ private fun VerifyCodeLayout(
connectionVerified: Boolean, connectionVerified: Boolean,
verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit,
) { ) {
ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) { ColumnWithScrollBar(
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,7 +130,10 @@ 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,7 +25,6 @@ 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.*
@@ -284,14 +283,9 @@ 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(
contentPadding = if (oneHandUI.value) { Modifier
PaddingValues(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp, bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()) .fillMaxWidth(),
} else {
PaddingValues(top = topPaddingToContent())
},
state = listState state = listState
) { ) {
item { item {
@@ -403,11 +397,6 @@ fun ModalData.GroupChatInfoLayout(
} }
} }
SectionBottomSpacer() SectionBottomSpacer()
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
}
}
if (!oneHandUI.value) {
NavigationBarBackground(oneHandUI.value, oneHandUI.value)
} }
} }
} }
@@ -119,7 +119,9 @@ 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,7 +313,10 @@ fun GroupMemberInfoLayout(
} }
} }
ColumnWithScrollBar { ColumnWithScrollBar(
Modifier
.fillMaxWidth(),
) {
Row( Row(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
@@ -82,7 +82,9 @@ 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,9 +82,10 @@ 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.imePadding(), modifier = Modifier.navigationBarsWithImePadding(),
sheetContent = { sheetContent = {
GetImageBottomSheet( GetImageBottomSheet(
chosenImage, chosenImage,
@@ -97,7 +98,9 @@ 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)
@@ -174,6 +177,7 @@ fun GroupProfileLayout(
} }
} }
} }
}
} }
private fun canUpdateProfile(displayName: String, groupProfile: GroupProfile): Boolean = private fun canUpdateProfile(displayName: String, groupProfile: GroupProfile): Boolean =
@@ -95,7 +95,9 @@ 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,7 +10,6 @@ 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
@@ -321,8 +320,6 @@ 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`
@@ -401,70 +398,6 @@ 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,6 +12,7 @@ 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
@@ -57,17 +58,9 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
val playersToRelease = rememberSaveable { mutableSetOf<URI>() } val playersToRelease = rememberSaveable { mutableSetOf<URI>() }
DisposableEffectOnGone( DisposableEffectOnGone(
always = { always = {
platform.androidSetStatusAndNavigationBarAppearance(false, false, blackNavBar = true) platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, Color.Black, false, false)
chatModel.fullscreenGalleryVisible.value = true
}, },
whenDispose = { whenGone = { playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) } }
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,7 +10,8 @@ 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.* import androidx.compose.ui.Alignment
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.*
@@ -33,7 +34,6 @@ 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,6 +41,7 @@ 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>) {
@@ -54,7 +55,7 @@ private fun showNewChatSheet(oneHandUI: State<Boolean>) {
chatModel.newChatSheetVisible.value = false chatModel.newChatSheetVisible.value = false
close() close()
} }
ModalView(close, showAppBar = !oneHandUI.value) { ModalView(close, closeOnTop = !oneHandUI.value) {
if (appPlatform.isAndroid) { if (appPlatform.isAndroid) {
BackHandler { BackHandler {
close() close()
@@ -121,7 +122,11 @@ 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())
}
) )
} }
} }
@@ -149,36 +154,74 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow<Animate
} }
} }
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
val listState = rememberLazyListState(lazyListState.first, lazyListState.second) Scaffold(
Box(Modifier.fillMaxSize()) { topBar = {
if (oneHandUI.value) { if (!oneHandUI.value) {
ChatListWithLoadingScreen(searchText, listState) Column {
Column(Modifier.align(Alignment.BottomCenter)) { ChatListToolbar(
ChatListToolbar( userPickerState,
userPickerState, stopped,
listState, setPerformLA,
stopped, )
setPerformLA, Divider()
) }
} }
} else { },
ChatListWithLoadingScreen(searchText, listState) bottomBar = {
Column { if (oneHandUI.value) {
ChatListToolbar( Column {
userPickerState, Divider()
listState, ChatListToolbar(
stopped, userPickerState,
setPerformLA, stopped,
) setPerformLA,
)
}
} }
if (searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) { },
NewChatSheetFloatingButton(oneHandUI, stopped) contentColor = LocalContentColor.current,
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 && !oneHandUI.value) { if (appPlatform.isDesktop) {
val call = remember { chatModel.activeCall }.value val call = remember { chatModel.activeCall }.value
if (call != null) { if (call != null) {
ActiveCallInteractiveArea(call) ActiveCallInteractiveArea(call)
@@ -196,46 +239,6 @@ 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(
@@ -253,7 +256,7 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
} }
@Composable @Composable
private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>, listState: LazyListState, stopped: Boolean, setPerformLA: (Boolean) -> Unit) { private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>, 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
@@ -262,18 +265,6 @@ 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(
@@ -332,9 +323,7 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
} }
} }
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
val scope = rememberCoroutineScope() DefaultTopAppBar(
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 {
@@ -362,14 +351,15 @@ 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)
} }
@@ -380,10 +370,10 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
) )
} }
}, },
onTitleClick = if (canScrollToZero.value) { { scrollToBottom(scope, listState) } } else null, onTitleClick = null,
onTop = !oneHandUI.value, showSearch = false,
onSearchValueChanged = {}, onSearchValueChanged = {},
buttons = { barButtons.forEach { it() } } buttons = barButtons
) )
} }
@@ -501,78 +491,74 @@ 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) } Icon(
Icon( painterResource(MR.images.ic_search),
painterResource(MR.images.ic_search), contentDescription = null,
contentDescription = null, Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(22.dp * fontSizeSqrtMultiplier),
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(22.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.secondary
tint = MaterialTheme.colors.secondary )
) SearchTextField(
SearchTextField( Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester),
Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester), placeholder = stringResource(MR.strings.search_or_paste_simplex_link),
placeholder = stringResource(MR.strings.search_or_paste_simplex_link), alwaysVisible = true,
alwaysVisible = true, searchText = searchText,
searchText = searchText, enabled = !remember { searchShowingSimplexLink }.value,
enabled = !remember { searchShowingSimplexLink }.value, trailingContent = null,
trailingContent = null, ) {
) { searchText.value = searchText.value.copy(it)
searchText.value = searchText.value.copy(it) }
val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } }
if (hasText.value) {
val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() }
BackHandler(onBack = hideSearchOnBack)
KeyChangeEffect(chatModel.currentRemoteHost.value) {
hideSearchOnBack()
} }
val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } } } else {
if (hasText.value) { val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() } if (chatModel.chats.value.isNotEmpty()) {
BackHandler(onBack = hideSearchOnBack) ToggleFilterEnabledButton()
KeyChangeEffect(chatModel.currentRemoteHost.value) {
hideSearchOnBack()
}
} else {
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
if (chatModel.chats.value.isNotEmpty()) {
ToggleFilterEnabledButton()
}
Spacer(Modifier.width(padding))
} }
val focusManager = LocalFocusManager.current Spacer(Modifier.width(padding))
val keyboardState = getKeyboardState() }
LaunchedEffect(keyboardState.value) { val focusManager = LocalFocusManager.current
if (keyboardState.value == KeyboardState.Closed && focused) { val keyboardState = getKeyboardState()
focusManager.clearFocus() LaunchedEffect(keyboardState.value) {
} if (keyboardState.value == KeyboardState.Closed && focused) {
} focusManager.clearFocus()
val view = LocalMultiplatformView()
LaunchedEffect(Unit) {
snapshotFlow { searchText.value.text }
.distinctUntilChanged()
.collect {
val link = strHasSingleSimplexLink(it.trim())
if (link != null) {
// if SimpleX link is pasted, show connection dialogue
hideKeyboard(view)
if (link.format is Format.SimplexLink) {
val linkText = link.simplexLinkText(link.format.linkType, link.format.smpHosts)
searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero)
}
searchShowingSimplexLink.value = true
searchChatFilteredBySimplexLink.value = null
connect(link.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() }
} else if (!searchShowingSimplexLink.value || it.isEmpty()) {
if (it.isNotEmpty()) {
// if some other text is pasted, enter search mode
focusRequester.requestFocus()
} else if (listState.layoutInfo.totalItemsCount > 0) {
listState.scrollToItem(0)
}
searchShowingSimplexLink.value = false
searchChatFilteredBySimplexLink.value = null
}
}
} }
} }
val oneHandUI = remember { appPrefs.oneHandUI.state } val view = LocalMultiplatformView()
Divider(Modifier.align(if (oneHandUI.value) Alignment.TopStart else Alignment.BottomStart)) LaunchedEffect(Unit) {
snapshotFlow { searchText.value.text }
.distinctUntilChanged()
.collect {
val link = strHasSingleSimplexLink(it.trim())
if (link != null) {
// if SimpleX link is pasted, show connection dialogue
hideKeyboard(view)
if (link.format is Format.SimplexLink) {
val linkText = link.simplexLinkText(link.format.linkType, link.format.smpHosts)
searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero)
}
searchShowingSimplexLink.value = true
searchChatFilteredBySimplexLink.value = null
connect(link.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() }
} else if (!searchShowingSimplexLink.value || it.isEmpty()) {
if (it.isNotEmpty()) {
// if some other text is pasted, enter search mode
focusRequester.requestFocus()
} else if (listState.layoutInfo.totalItemsCount > 0) {
listState.scrollToItem(0)
}
searchShowingSimplexLink.value = false
searchChatFilteredBySimplexLink.value = null
}
}
}
} }
} }
@@ -604,37 +590,8 @@ enum class ScrollDirection {
} }
@Composable @Composable
fun BoxScope.StatusBarBackground() { private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldValue>) {
if (appPlatform.isAndroid) { val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
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) }
@@ -671,45 +628,40 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
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(
if (!oneHandUI.value) Modifier.imePadding() else Modifier, Modifier.fillMaxSize(),
listState, listState,
reverseLayout = oneHandUI.value reverseLayout = oneHandUI.value
) { ) {
item { Spacer(Modifier.height(blankSpaceSize)) }
stickyHeader { stickyHeader {
Column( Column(
Modifier Modifier
.zIndex(1f)
.offset { .offset {
val offsetMultiplier = if (oneHandUI.value) 1 else -1 val y = if (searchText.value.text.isEmpty()) {
val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened) || scrollDirection == ScrollDirection.Up) { val offsetMultiplier = if (oneHandUI.value) 1 else -1
if (listState.firstVisibleItemIndex == 0) -offsetMultiplier * listState.firstVisibleItemScrollOffset if (
else -offsetMultiplier * blankSpaceSize.roundToPx() (oneHandUI.value && scrollDirection == ScrollDirection.Up) ||
(appPlatform.isAndroid && keyboardState == KeyboardState.Opened)
) {
0
} else if (listState.firstVisibleItemIndex == 0) offsetMultiplier * listState.firstVisibleItemScrollOffset else offsetMultiplier * 1000
} else { } else {
when (listState.firstVisibleItemIndex) { 0
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) {
Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) { Divider()
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) }
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
} if (!oneHandUI.value) {
} else { Divider()
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
} }
} }
} }
if (!oneHandUICardShown.value && chats.size > 1) { if (appPlatform.isAndroid && !oneHandUICardShown.value && chats.count() > 1) {
item { item {
ToggleChatListCard() ToggleChatListCard()
} }
@@ -720,30 +672,17 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
} } } }
ChatListNavLinkView(chat, nextChatSelected) ChatListNavLinkView(chat, nextChatSelected)
} }
if (!oneHandUICardShown.value && chats.size <= 1) { if (appPlatform.isAndroid && !oneHandUICardShown.value && chats.count() <= 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().imePadding(), contentAlignment = Alignment.Center) { Box(Modifier.fillMaxSize(), 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(
@@ -788,7 +727,3 @@ 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,7 +620,9 @@ 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),
@@ -643,7 +645,9 @@ 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(
@@ -667,7 +671,9 @@ 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(
@@ -691,7 +697,9 @@ 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(
@@ -707,7 +715,9 @@ 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,13 +11,10 @@ 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
@@ -25,7 +22,26 @@ 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
@@ -53,24 +69,22 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
} }
null -> {} null -> {}
} }
if (chatModel.chats.value.isNotEmpty()) { Box(Modifier.padding(it)) {
ShareList( Column(
chatModel, modifier = Modifier.fillMaxSize()
search = searchInList, ) {
isMediaOrFileAttachment = isMediaOrFileAttachment, if (chatModel.chats.value.isNotEmpty()) {
isVoice = isVoice, ShareList(
hasSimplexLink = hasSimplexLink, chatModel,
) search = searchInList,
} else { isMediaOrFileAttachment = isMediaOrFileAttachment,
EmptyList() isVoice = isVoice,
} hasSimplexLink = hasSimplexLink,
if (oneHandUI.value) { )
StatusBarBackground() } else {
} else { EmptyList()
NavigationBarBackground(oneHandUI.value, true) }
} }
Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) {
ShareListToolbar(chatModel, stopped) { searchInList = it.trim() }
} }
} }
} }
@@ -94,6 +108,7 @@ 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 {
@@ -103,13 +118,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(keyboardCoversBar = false) { close -> ModalManager.start.showCustomModal { close ->
val search = rememberSaveable { mutableStateOf("") } val search = rememberSaveable { mutableStateOf("") }
ModalView( ModalView(
{ close() }, { close() },
showSearch = true, endButtons = {
searchAlwaysVisible = true, SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
onSearchValueChanged = { search.value = it }, },
content = { content = {
ActiveProfilePicker( ActiveProfilePicker(
search = search, search = search,
@@ -133,8 +148,31 @@ 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,
)
}
}
}
DefaultAppBar( DefaultTopAppBar(
navigationButton = navButton, navigationButton = navButton,
title = { title = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@@ -153,29 +191,8 @@ 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 = { buttons = barButtons
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,
)
}
}
}
) )
} }
@@ -194,13 +211,8 @@ 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.then(if (oneHandUI.value) Modifier.consumeWindowInsets(WindowInsets.navigationBars.only(WindowInsetsSides.Vertical)) else Modifier).imePadding(), modifier = Modifier.fillMaxSize(),
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,7 +23,6 @@ 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.*
@@ -138,16 +137,12 @@ 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, ambientColor = background) else Modifier) .then(if (newChat.isVisible()) Modifier.shadow(8.dp, clip = true) else Modifier)
.padding(top = if (appPlatform.isDesktop && oneHandUI.value) 7.dp else 0.dp) .background(if (appPlatform.isAndroid) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha()) else MaterialTheme.colors.surface)
.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
) { ) {
@@ -203,13 +198,12 @@ 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, iconColor = iconColor, stopped = stopped) ProfilePreview(currentUser.profile, stopped = stopped)
} }
} }
} }
@@ -240,7 +234,6 @@ 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
) )
@@ -272,15 +265,13 @@ 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(keyboardCoversBar = false) { close -> ModalManager.start.showCustomModal { close ->
val search = rememberSaveable { mutableStateOf("") } val search = rememberSaveable { mutableStateOf("") }
val profileHidden = rememberSaveable { mutableStateOf(false) } val profileHidden = rememberSaveable { mutableStateOf(false) }
ModalView( ModalView(
{ close() }, { close() },
showSearch = true, endButtons = {
searchAlwaysVisible = true, SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
onSearchValueChanged = {
search.value = it
}, },
content = { UserProfilesView(chatModel, search, profileHidden) }) content = { UserProfilesView(chatModel, search, profileHidden) })
} }
@@ -528,7 +519,6 @@ 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,7 +46,9 @@ 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(maxIntrinsicSize = true) { ColumnWithScrollBar(Modifier.fillMaxWidth(), maxIntrinsicSize = true) {
Layout() Layout()
} }
} }
@@ -77,7 +77,10 @@ fun DatabaseErrorView(
Text(String.format(generalGetString(MR.strings.database_migrations), ms.joinToString(", "))) Text(String.format(generalGetString(MR.strings.database_migrations), ms.joinToString(", ")))
} }
ColumnWithScrollBarNoAppBar(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) { ColumnWithScrollBar(
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,7 +156,9 @@ 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) {
@@ -1,71 +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.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
)
}
}
@@ -1,139 +0,0 @@
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,13 +1,11 @@
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
@@ -383,14 +381,7 @@ private fun drawToBitmap(image: ImageBitmap, imageScale: Float, tint: Color, siz
return bitmap return bitmap
} }
fun CacheDrawScope.chatViewBackground( fun CacheDrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperType, background: Color, tint: Color): DrawResult {
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) {
@@ -405,55 +396,53 @@ fun CacheDrawScope.chatViewBackground(
} }
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) { is WallpaperType.Preset -> drawImage(image)
is WallpaperType.Preset -> drawImage(image) is WallpaperType.Image -> when (val scaleType = imageType.scaleType ?: WallpaperScaleType.FILL) {
is WallpaperType.Image -> when (val scaleType = imageType.scaleType ?: WallpaperScaleType.FILL) { WallpaperScaleType.REPEAT -> drawImage(image)
WallpaperScaleType.REPEAT -> drawImage(image) WallpaperScaleType.FILL, WallpaperScaleType.FIT -> {
WallpaperScaleType.FILL, WallpaperScaleType.FIT -> { clipRect {
clipRect { val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height))
val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height)) val scaledWidth = (image.width * scale.scaleX).roundToInt()
val scaledWidth = (image.width * scale.scaleX).roundToInt() val scaledHeight = (image.height * scale.scaleY).roundToInt()
val scaledHeight = (image.height * scale.scaleY).roundToInt() // Large image will cause freeze
// Large image will cause freeze if (image.width > 4320 || image.height > 4320) return@clipRect
if (image.width > 4320 || image.height > 4320) return@clipRect
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
if (scaleType == WallpaperScaleType.FIT) { if (scaleType == WallpaperScaleType.FIT) {
if (scaledWidth < size.width) { if (scaledWidth < size.width) {
// has black lines at left and right sides // has black lines at left and right sides
var x = (size.width - scaledWidth) / 2 var x = (size.width - scaledWidth) / 2
while (x > 0) { while (x > 0) {
drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
x -= scaledWidth x -= scaledWidth
} }
x = size.width - (size.width - scaledWidth) / 2 x = size.width - (size.width - scaledWidth) / 2
while (x < size.width) { while (x < size.width) {
drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
x += scaledWidth x += scaledWidth
} }
} else { } else {
// has black lines at top and bottom sides // has black lines at top and bottom sides
var y = (size.height - scaledHeight) / 2 var y = (size.height - scaledHeight) / 2
while (y > 0) { while (y > 0) {
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
y -= scaledHeight y -= scaledHeight
} }
y = size.height - (size.height - scaledHeight) / 2 y = size.height - (size.height - scaledHeight) / 2
while (y < size.height) { while (y < size.height) {
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
y += scaledHeight y += scaledHeight
}
} }
} }
} }
drawRect(tint)
} }
drawRect(tint)
} }
is WallpaperType.Empty -> {}
} }
is WallpaperType.Empty -> {}
} }
} }
} }
@@ -19,8 +19,6 @@ 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()
@@ -0,0 +1,181 @@
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,67 +3,15 @@ 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.IntSize import androidx.compose.ui.unit.Velocity
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)
) { ) {
@@ -76,8 +24,6 @@ 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,6 +3,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.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.*
@@ -21,11 +22,13 @@ 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,7 +14,6 @@ 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)?
) { ) {
@@ -24,7 +23,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,120 +3,44 @@ 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.* import androidx.compose.runtime.Composable
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.platform.LocalDensity import androidx.compose.ui.unit.Dp
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 chat.simplex.common.model.ChatController.appPrefs import androidx.compose.ui.unit.dp
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 DefaultAppBar( fun DefaultTopAppBar(
navigationButton: (@Composable RowScope.() -> Unit)? = null, navigationButton: (@Composable RowScope.() -> Unit)? = null,
title: (@Composable () -> Unit)? = null, title: (@Composable () -> Unit)?,
fixedTitleText: String? = null,
onTitleClick: (() -> Unit)? = null, onTitleClick: (() -> Unit)? = null,
onTop: Boolean, showSearch: Boolean,
showSearch: Boolean = false, onSearchValueChanged: (String) -> Unit,
searchAlwaysVisible: Boolean = false, buttons: List<@Composable RowScope.() -> Unit> = emptyList(),
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.imePadding() } else Modifier
val themeBackgroundMix = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) TopAppBar(
val prefAlpha = remember { appPrefs.inAppBarsAlpha.state } modifier = modifier,
val handler = LocalAppBarHandler.current title = {
val connection = LocalAppBarHandler.current?.connection if (!showSearch) {
val titleText = remember(handler?.title?.value, fixedTitleText) { title?.invoke()
if (fixedTitleText != null) { } else {
mutableStateOf(fixedTitleText) SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = false, onValueChange = onSearchValueChanged)
} else {
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( backgroundColor = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f),
Modifier navigationIcon = navigationButton,
.fillMaxWidth() buttons = if (!showSearch) buttons else emptyList(),
.then(if (!onTop) Modifier.navigationBarsPadding() else Modifier) centered = !showSearch,
.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
)
}
}
},
navigationIcon = navigationButton,
buttons = if (!showSearch) buttons else {{}},
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
) )
} }
@@ -159,107 +83,58 @@ fun NavigationButtonMenu(onButtonClicked: () -> Unit) {
} }
@Composable @Composable
private fun BoxScope.AppBarDivider(onTop: Boolean, fixedAlpha: Boolean, connection: CollapsingAppBarNestedScrollConnection?) { private fun TopAppBar(
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: @Composable RowScope.() -> Unit = {}, buttons: List<@Composable RowScope.() -> Unit> = emptyList(),
backgroundColor: Color = MaterialTheme.colors.primarySurface,
centered: Boolean, centered: Boolean,
onTop: Boolean,
) { ) {
val adjustedModifier = modifier Box(
.then(if (onTop) Modifier.statusBarsPadding() else Modifier) modifier
.height(AppBarHeight * fontSizeSqrtMultiplier) .fillMaxWidth()
.fillMaxWidth() .height(AppBarHeight * fontSizeSqrtMultiplier)
.padding(horizontal = AppBarHorizontalPadding) .background(backgroundColor)
if (centered) { .padding(horizontal = 4.dp),
AppBarCenterAligned(adjustedModifier, title, navigationIcon, buttons) contentAlignment = Alignment.CenterStart,
} 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) {
navigationIcon()
Spacer(Modifier.width(AppBarHorizontalPadding))
} else {
Spacer(Modifier.width(DEFAULT_PADDING))
}
Row(Modifier
.weight(1f)
.padding(end = DEFAULT_PADDING_HALF)
) {
title()
}
Row(
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
buttons()
}
}
}
@Composable
private fun AppBarCenterAligned(
modifier: Modifier,
title: @Composable () -> Unit,
navigationIcon: @Composable (RowScope.() -> Unit)? = null,
buttons: @Composable RowScope.() -> Unit,
) {
CenteredRowLayout(modifier) {
if (navigationIcon != null) { if (navigationIcon != null) {
Row( Row(
Modifier.padding(end = AppBarHorizontalPadding), Modifier
.fillMaxHeight()
.width(TitleInsetWithIcon - AppBarHorizontalPadding),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
content = navigationIcon content = navigationIcon
) )
} else {
Spacer(Modifier)
}
Row(
Modifier.padding(end = DEFAULT_PADDING_HALF)
) {
title()
} }
Row( Row(
Modifier
.fillMaxHeight()
.fillMaxWidth(),
horizontalArrangement = Arrangement.End, horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
buttons() buttons.forEach { it() }
}
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()
} }
} }
} }
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 = 2.dp val AppBarHorizontalPadding = 4.dp
val BottomAppBarHeight = 60.dp
private val TitleInsetWithoutIcon = DEFAULT_PADDING - AppBarHorizontalPadding
val TitleInsetWithIcon = 72.dp
@@ -6,6 +6,7 @@ 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
@@ -106,7 +107,6 @@ 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 = ripple, indication = rememberRipple(bounded = false, radius = boxSize / 2, color = background.lighter(0.1f)),
enabled = enabled.value enabled = enabled.value
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -6,14 +6,12 @@ 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
@@ -23,40 +21,24 @@ 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 = Color.Unspecified, background: Color = MaterialTheme.colors.background,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showSearch: Boolean = false, closeOnTop: Boolean = true,
searchAlwaysVisible: Boolean = false,
onSearchValueChanged: (String) -> Unit = {},
endButtons: @Composable RowScope.() -> Unit = {}, endButtons: @Composable RowScope.() -> Unit = {},
content: @Composable BoxScope.() -> Unit, content: @Composable () -> Unit,
) { ) {
if (showClose && showAppBar) { if (showClose) {
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) {
Box(if (background != Color.Unspecified) Modifier.background(background) else Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) { Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) {
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
)
}
}
} }
} }
} }
@@ -65,7 +47,7 @@ enum class ModalPlacement {
START, CENTER, END, FULLSCREEN START, CENTER, END, FULLSCREEN
} }
class ModalData(val keyboardCoversBar: Boolean = true) { class ModalData() {
private val state = mutableMapOf<String, MutableState<Any?>>() private val state = mutableMapOf<String, MutableState<Any?>>()
fun <T> stateGetOrPut (key: String, default: () -> T): MutableState<T> = fun <T> stateGetOrPut (key: String, default: () -> T): MutableState<T> =
state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState<T> state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState<T>
@@ -73,7 +55,7 @@ class ModalData(val keyboardCoversBar: Boolean = true) {
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(null, null, keyboardCoversBar = keyboardCoversBar) val appBarHandler = AppBarHandler()
} }
class ModalManager(private val placement: ModalPlacement? = null) { class ModalManager(private val placement: ModalPlacement? = null) {
@@ -87,21 +69,23 @@ 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, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) { fun showModal(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
val data = ModalData()
showCustomModal { close -> showCustomModal { close ->
ModalView(close, showClose = showClose, endButtons = endButtons, content = { content() }) ModalView(close, showClose = showClose, closeOnTop = closeOnTop, endButtons = endButtons, content = { data.content() })
} }
} }
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) { fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, closeOnTop: 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, content = { content(close) }) ModalView(close, showClose = showClose, endButtons = endButtons, closeOnTop = closeOnTop, content = { data.content(close) })
} }
} }
fun showCustomModal(animated: Boolean = true, keyboardCoversBar: Boolean = true, modal: @Composable ModalData.(close: () -> Unit) -> Unit) { fun showCustomModal(animated: Boolean = true, modal: @Composable ModalData.(close: () -> Unit) -> Unit) {
Log.d(TAG, "ModalManager.showCustomModal") Log.d(TAG, "ModalManager.showCustomModal")
val data = ModalData(keyboardCoversBar = keyboardCoversBar) val data = ModalData()
// 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()) {
@@ -162,7 +146,9 @@ 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(LocalAppBarHandler provides adjustAppBarHandler(it.second.appBarHandler)) { CompositionLocalProvider(
LocalAppBarHandler provides it.second.appBarHandler
) {
it.third(it.second, ::closeModal) it.third(it.second, ::closeModal)
} }
} }
@@ -178,7 +164,9 @@ class ModalManager(private val placement: ModalPlacement? = null) {
} }
) { ) {
modalViews.getOrNull(it - 1)?.let { modalViews.getOrNull(it - 1)?.let {
CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.second.appBarHandler)) { CompositionLocalProvider(
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.* import androidx.compose.foundation.layout.defaultMinSize
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,9 +18,12 @@ 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.* import androidx.compose.ui.unit.dp
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
@@ -35,7 +38,6 @@ 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() }
@@ -79,20 +81,15 @@ 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 = if (appPlatform.isAndroid) TextFieldDefaults.MinHeight else 0.dp minHeight = TextFieldDefaults.MinHeight
), ),
onValueChange = { onValueChange = {
searchText.value = it searchText.value = it
@@ -103,14 +100,18 @@ 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, style = textStyle.copy(color = MaterialTheme.colors.secondary), maxLines = 1, overflow = TextOverflow.Ellipsis) Text(placeholder, maxLines = 1, overflow = TextOverflow.Ellipsis)
}, },
trailingIcon = if (searchText.value.text.isNotEmpty()) {{ trailingIcon = if (searchText.value.text.isNotEmpty()) {{
IconButton({ IconButton({
@@ -120,7 +121,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,6 +57,7 @@ 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)
@@ -86,7 +87,6 @@ fun TextEditor(
enabled = true, enabled = true,
isError = false, isError = false,
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified)
) )
} }
) )
@@ -32,7 +32,10 @@ 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 } }
@@ -228,7 +231,10 @@ 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,7 +149,9 @@ private fun MigrateFromDeviceLayout(
) { ) {
val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) } val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) }
ColumnWithScrollBar(maxIntrinsicSize = true) { ColumnWithScrollBar(
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,7 +162,9 @@ private fun ModalData.MigrateToDeviceLayout(
close: () -> Unit, close: () -> Unit,
) { ) {
val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) } val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) }
ColumnWithScrollBar(maxIntrinsicSize = true) { ColumnWithScrollBar(
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,7 +15,9 @@ import chat.simplex.res.MR
@Composable @Composable
fun AddContactLearnMore(close: () -> Unit) { fun AddContactLearnMore(close: () -> Unit) {
ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) { ColumnWithScrollBar(
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,9 +84,10 @@ 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.imePadding(), modifier = Modifier.navigationBarsWithImePadding(),
sheetContent = { sheetContent = {
GetImageBottomSheet( GetImageBottomSheet(
chosenImage, chosenImage,
@@ -99,7 +100,11 @@ 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
@@ -117,7 +122,7 @@ fun AddGroupLayout(
} }
} }
} }
Row(Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_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.group_display_name_field), stringResource(MR.strings.group_display_name_field),
fontSize = 16.sp fontSize = 16.sp
@@ -129,9 +134,7 @@ 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(
@@ -167,6 +170,7 @@ 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.end.showModal { IncognitoView() } }, click = { ModalManager.start.showModal { IncognitoView() } },
iconColor = Indigo, iconColor = Indigo,
extraPadding = false extraPadding = false
) { ) {
@@ -105,7 +105,9 @@ 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,7 +1,9 @@
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
@@ -12,7 +14,8 @@ 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.* import androidx.compose.ui.Alignment
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
@@ -29,43 +32,56 @@ 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.chat.topPaddingToContent import chat.simplex.common.views.chatlist.ScrollDirection
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 } }
Box { Scaffold(
val closeAll = { ModalManager.start.closeModals() } bottomBar = {
if (showToolbarInOneHandUI.value) {
Column(modifier = Modifier.fillMaxSize()) { Column {
NewChatSheetLayout( Divider()
addContact = { CloseSheetBar(
ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll) } close = close,
}, showClose = true,
scanPaste = { endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) },
ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = closeAll) } arrangement = Arrangement.Bottom,
}, closeBarTitle = generalGetString(MR.strings.new_message),
createGroup = { barPaddingValues = PaddingValues(horizontal = 0.dp)
ModalManager.start.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) } )
}, }
rh = rh, }
close = close
)
} }
if (oneHandUI.value) { ) {
Column(Modifier.align(Alignment.BottomCenter)) { Column(
DefaultAppBar( modifier = Modifier.fillMaxSize().padding(it)
navigationButton = { NavigationButtonBack(onButtonClicked = close) }, ) {
fixedTitleText = generalGetString(MR.strings.new_message), val closeAll = { ModalManager.start.closeModals() }
onTop = false,
Column(modifier = Modifier.fillMaxSize()) {
NewChatSheetLayout(
addContact = {
ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll ) }
},
scanPaste = {
ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = closeAll) }
},
createGroup = {
ModalManager.start.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) }
},
rh = rh,
close = close
) )
} }
} }
@@ -171,258 +187,168 @@ private fun ModalData.NewChatSheetLayout(
derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) } derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) }
} }
val actionButtonsOriginal = listOf( LazyColumnWithScrollBar(
Triple( Modifier.fillMaxSize(),
painterResource(MR.images.ic_add_link), listState,
stringResource(MR.strings.add_contact_tab), reverseLayout = oneHandUI.value
addContact, ) {
), if (!oneHandUI.value) {
Triple( item {
painterResource(MR.images.ic_qr_code), Box(contentAlignment = Alignment.Center) {
if (appPlatform.isAndroid) stringResource(MR.strings.scan_paste_link) else stringResource(MR.strings.paste_link), val bottomPadding = DEFAULT_PADDING
scanPaste, AppBarTitle(
), stringResource(MR.strings.new_message),
Triple( hostDevice(rh?.remoteHostId),
painterResource(MR.images.ic_group), bottomPadding = bottomPadding
stringResource(MR.strings.create_group_button),
createGroup,
)
)
@Composable
fun DeletedChatsItem(actionButtons: List<Triple<Painter, String, () -> Unit>>) {
if (searchText.value.text.isEmpty()) {
Spacer(Modifier.padding(bottom = 27.dp))
}
if (searchText.value.text.isEmpty()) {
Row {
SectionView {
actionButtons.map {
NewChatButton(
icon = it.first,
text = it.second,
click = it.third,
)
}
}
}
if (deletedChats.isNotEmpty()) {
SectionDividerSpaced(maxBottomPadding = false)
SectionView {
SectionItemView(
click = {
ModalManager.start.showCustomModal { closeDeletedChats ->
ModalView(
close = closeDeletedChats,
showAppBar = !oneHandUI.value,
) {
if (oneHandUI.value) {
BackHandler(onBack = closeDeletedChats)
}
DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = {
ModalManager.start.closeModals()
})
}
}
}
) {
Icon(
painterResource(MR.images.ic_inventory_2),
contentDescription = stringResource(MR.strings.deleted_chats),
tint = MaterialTheme.colors.secondary,
)
TextIconSpaced(false)
Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground)
}
}
}
}
}
@Composable
fun NoFilteredContactsItem() {
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Text(
generalGetString(MR.strings.no_filtered_contacts),
color = MaterialTheme.colors.secondary
) )
} }
} }
} }
} stickyHeader {
Column(
Modifier
.offset {
val y = if (searchText.value.text.isEmpty()) {
val offsetMultiplier = if (oneHandUI.value) 1 else -1
@Composable if (
fun OneHandLazyColumn() { (oneHandUI.value && scrollDirection == ScrollDirection.Up) ||
val blankSpaceSize = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier (appPlatform.isAndroid && keyboardState == KeyboardState.Opened)
LazyColumnWithScrollBar( ) {
state = listState, 0
reverseLayout = oneHandUI.value } else if (oneHandUI.value && listState.firstVisibleItemIndex == 0) {
) { listState.firstVisibleItemScrollOffset
item { Spacer(Modifier.height(blankSpaceSize)) } } else if (!oneHandUI.value && listState.firstVisibleItemIndex == 0) {
stickyHeader { 0
val scrolledSomething by remember { derivedStateOf { listState.firstVisibleItemScrollOffset > 0 || listState.firstVisibleItemIndex > 0 } } } else if (!oneHandUI.value && listState.firstVisibleItemIndex == 1) {
Column( -listState.firstVisibleItemScrollOffset
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 { } else {
when (listState.firstVisibleItemIndex) { offsetMultiplier * 1000
0 -> 0 }
1 -> listState.firstVisibleItemScrollOffset } else {
else -> 1000 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(
Triple(
painterResource(MR.images.ic_add_link),
stringResource(MR.strings.add_contact_tab),
addContact,
),
Triple(
painterResource(MR.images.ic_qr_code),
if (appPlatform.isAndroid) stringResource(MR.strings.scan_paste_link) else stringResource(MR.strings.paste_link),
scanPaste,
),
Triple(
painterResource(MR.images.ic_group),
stringResource(MR.strings.create_group_button),
createGroup,
)
)
val actionButtons by remember(oneHandUI.value) {
derivedStateOf {
if (oneHandUI.value) actionButtonsOriginal.asReversed() else actionButtonsOriginal
}
}
if (searchText.value.text.isEmpty()) {
Row {
SectionView {
actionButtons.map {
NewChatButton(
icon = it.first,
text = it.second,
click = it.third,
)
}
}
}
if (deletedChats.isNotEmpty()) {
SectionDividerSpaced(maxBottomPadding = false)
SectionView {
SectionItemView(
click = {
ModalManager.start.showCustomModal { closeDeletedChats ->
ModalView(
close = closeDeletedChats,
closeOnTop = !oneHandUI.value,
) {
DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = {
ModalManager.start.closeModals()
})
}
} }
} }
IntOffset(0, y) ) {
Icon(
painterResource(MR.images.ic_inventory_2),
contentDescription = stringResource(MR.strings.deleted_chats),
tint = MaterialTheme.colors.secondary,
)
TextIconSpaced(false)
Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground)
} }
// 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 {
item { if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) {
if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) { if (!oneHandUI.value) {
SectionDividerSpaced()
SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
} else {
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {} SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
Spacer(Modifier.height(DEFAULT_PADDING_HALF)) Spacer(Modifier.height(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.windowInsetsTopHeight(WindowInsets.statusBars))
}
}
} }
}
@Composable item {
fun NonOneHandLazyColumn() { if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
val blankSpaceSize = topPaddingToContent() Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) {
LazyColumnWithScrollBar( Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Modifier.imePadding(), Text(
state = listState, generalGetString(MR.strings.no_filtered_contacts),
reverseLayout = false color = MaterialTheme.colors.secondary
) {
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 { itemsIndexed(filteredContactChats) { index, chat ->
if (oneHandUI.value) { val nextChatSelected = remember(chat.id, filteredContactChats) {
OneHandLazyColumn() derivedStateOf {
StatusBarBackground() chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
} else { }
NonOneHandLazyColumn() }
NavigationBarBackground(oneHandUI.value, true) ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true)
} }
} }
} }
@@ -628,7 +554,26 @@ 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 }
Box { val keyboardState by getKeyboardState()
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) }
@@ -645,93 +590,57 @@ private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats
contactChats = allChats contactChats = allChats
) )
Box { LazyColumnWithScrollBar(
val topPaddingToContent = topPaddingToContent() Modifier.fillMaxSize(),
LazyColumnWithScrollBar( contentPadding = contentPadding,
if (!oneHandUI.value) Modifier.imePadding() else Modifier, reverseLayout = oneHandUI.value,
contentPadding = PaddingValues( ) {
top = if (!oneHandUI.value) topPaddingToContent else 0.dp, item {
bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp if (!oneHandUI.value) {
), Box(contentAlignment = Alignment.Center) {
reverseLayout = oneHandUI.value, val bottomPadding = DEFAULT_PADDING
) { AppBarTitle(
item { stringResource(MR.strings.deleted_chats),
if (!oneHandUI.value) { hostDevice(rh?.remoteHostId),
Box(contentAlignment = Alignment.Center) { bottomPadding = bottomPadding
val bottomPadding = DEFAULT_PADDING )
AppBarTitle(
stringResource(MR.strings.deleted_chats),
hostDevice(rh?.remoteHostId),
bottomPadding = bottomPadding
)
}
} }
} }
item { }
if (!oneHandUI.value) { item {
Divider() if (!oneHandUI.value) {
ContactsSearchBar(
listState = listState,
searchText = searchText,
searchShowingSimplexLink = searchShowingSimplexLink,
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
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()
} }
ContactsSearchBar(
item { listState = listState,
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) { searchText = searchText,
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) { searchShowingSimplexLink = searchShowingSimplexLink,
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
Text( close = close,
generalGetString(MR.strings.no_filtered_contacts),
color = MaterialTheme.colors.secondary,
)
}
}
}
}
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 = 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,
) )
Divider()
}
item {
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Text(
generalGetString(MR.strings.no_filtered_contacts),
color = MaterialTheme.colors.secondary,
)
}
}
}
}
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 = false)
} }
} }
} }
@@ -29,12 +29,10 @@ 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
@@ -400,12 +398,8 @@ fun ActiveProfilePicker(
.fillMaxSize() .fillMaxSize()
.alpha(if (progressByTimeout) 0.6f else 1f) .alpha(if (progressByTimeout) 0.6f else 1f)
) { ) {
LazyColumnWithScrollBar(Modifier.padding(top = topPaddingToContent()), userScrollEnabled = !switchingProfile.value) { LazyColumnWithScrollBar(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 }
@@ -440,9 +434,6 @@ fun ActiveProfilePicker(
ProfilePickerUserOption(p) ProfilePickerUserOption(p)
} }
} }
item {
Spacer(Modifier.imePadding().padding(bottom = DEFAULT_BOTTOM_PADDING))
}
} }
} }
if (progressByTimeout) { if (progressByTimeout) {
@@ -481,13 +472,13 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection
end = 16.dp end = 16.dp
), ),
click = { click = {
ModalManager.start.showCustomModal(keyboardCoversBar = false) { close -> ModalManager.start.showCustomModal { close ->
val search = rememberSaveable { mutableStateOf("") } val search = rememberSaveable { mutableStateOf("") }
ModalView( ModalView(
{ close() }, { close() },
showSearch = true, endButtons = {
searchAlwaysVisible = true, SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
onSearchValueChanged = { search.value = it }, },
content = { content = {
ActiveProfilePicker( ActiveProfilePicker(
search = search, search = search,
@@ -625,7 +616,6 @@ 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,9 +76,15 @@ private fun CreateSimpleXAddressLayout(
createAddress: () -> Unit, createAddress: () -> Unit,
nextStep: () -> Unit, nextStep: () -> Unit,
) { ) {
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { val handler = remember { AppBarHandler() }
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,7 +23,11 @@ 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(Modifier.padding(DEFAULT_PADDING)) { ColumnWithScrollBar(
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,16 +7,21 @@ 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
@@ -54,32 +59,34 @@ private fun LinkAMobileLayout(
staleQrCode: MutableState<Boolean>, staleQrCode: MutableState<Boolean>,
updateDeviceName: (String) -> Unit, updateDeviceName: (String) -> Unit,
) { ) {
ModalView({ appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) }) { Column(Modifier.themedBackground()) {
Column(Modifier.fillMaxSize().padding(top = AppBarHeight * fontSizeSqrtMultiplier)) { CloseSheetBar(close = {
Box(Modifier.align(Alignment.CenterHorizontally)) { appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) })
} BackHandler(onBack = {
Row(Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING * 2), verticalAlignment = Alignment.CenterVertically) { appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
Column( })
Modifier.weight(0.3f), AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles))
verticalArrangement = Arrangement.spacedBy(8.dp) Row(Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING * 2), verticalAlignment = Alignment.CenterVertically) {
) { Column(
SectionView(generalGetString(MR.strings.this_device_name).uppercase()) { Modifier.weight(0.3f),
DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) } verticalArrangement = Arrangement.spacedBy(8.dp)
SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile)) ) {
PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), checked = remember { ChatModel.controller.appPrefs.offerRemoteMulticast.state }.value) { SectionView(generalGetString(MR.strings.this_device_name).uppercase()) {
ChatModel.controller.appPrefs.offerRemoteMulticast.set(it) DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) }
} SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile))
PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), checked = remember { ChatModel.controller.appPrefs.offerRemoteMulticast.state }.value) {
ChatModel.controller.appPrefs.offerRemoteMulticast.set(it)
} }
} }
Box(Modifier.weight(0.7f)) { }
AddingMobileDevice(false, staleQrCode, connecting) { Box(Modifier.weight(0.7f)) {
// currentRemoteHost will be set instantly but remoteHosts may be delayed AddingMobileDevice(false, staleQrCode, connecting) {
if (chatModel.remoteHosts.isEmpty() && chatModel.currentRemoteHost.value == null) { // currentRemoteHost will be set instantly but remoteHosts may be delayed
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) if (chatModel.remoteHosts.isEmpty() && chatModel.currentRemoteHost.value == null) {
} else { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete) } else {
} chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete)
} }
} }
} }
@@ -25,9 +25,16 @@ import chat.simplex.res.MR
@Composable @Composable
fun SetNotificationsMode(m: ChatModel) { fun SetNotificationsMode(m: ChatModel) {
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { val handler = remember { AppBarHandler() }
CompositionLocalProvider(
LocalAppBarHandler provides handler
) {
ModalView({}, showClose = false) { ModalView({}, showClose = false) {
ColumnWithScrollBar(Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) { ColumnWithScrollBar(
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,10 +104,13 @@ private fun SetupDatabasePassphraseLayout(
onConfirmEncrypt: () -> Unit, onConfirmEncrypt: () -> Unit,
nextStep: () -> Unit, nextStep: () -> Unit,
) { ) {
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { val handler = remember { AppBarHandler() }
CompositionLocalProvider(
LocalAppBarHandler provides handler
) {
ModalView({}, showClose = false) { ModalView({}, showClose = false) {
ColumnWithScrollBar( ColumnWithScrollBar(
Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer).padding(bottom = DEFAULT_PADDING * 2), Modifier.fillMaxSize().themedBackground().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,17 +31,15 @@ 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)
}
}) {
SimpleXInfoLayout(
user = chatModel.currentUser.value,
onboardingStage = chatModel.controller.appPrefs.onboardingStage
)
} }
}) {
SimpleXInfoLayout(
user = chatModel.currentUser.value,
onboardingStage = chatModel.controller.appPrefs.onboardingStage
)
} }
} else { } else {
SimpleXInfoLayout( SimpleXInfoLayout(
@@ -58,6 +56,7 @@ fun SimpleXInfoLayout(
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar(
Modifier Modifier
.fillMaxSize()
.padding(horizontal = DEFAULT_PADDING), .padding(horizontal = DEFAULT_PADDING),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
@@ -119,10 +119,11 @@ 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), withPadding = false, bottomPadding = DEFAULT_PADDING) AppBarTitle(String.format(generalGetString(MR.strings.new_in_version), v.version), bottomPadding = DEFAULT_PADDING)
v.features.forEach { feature -> v.features.forEach { feature ->
if (feature.show) { if (feature.show) {
@@ -74,7 +74,9 @@ 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)
@@ -406,7 +408,9 @@ 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 { ColumnWithScrollBar(Modifier.fillMaxWidth()) {
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,15 +176,7 @@ private fun ConnectMobileViewLayout(
refreshQrCode: () -> Unit = {}, refreshQrCode: () -> Unit = {},
UnderQrLayout: @Composable () -> Unit = {}, UnderQrLayout: @Composable () -> Unit = {},
) { ) {
@Composable ColumnWithScrollBar(Modifier.fillMaxWidth()) {
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,7 +202,10 @@ 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) {
@@ -325,7 +328,9 @@ 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)
} }
@@ -360,7 +365,9 @@ 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,11 +4,9 @@ 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
@@ -41,7 +39,6 @@ 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
@@ -88,115 +85,28 @@ 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() {
BoxWithConstraints { SectionView(stringResource(MR.strings.settings_section_title_message_shape).uppercase(), contentPadding = PaddingValues()) {
SectionView(stringResource(MR.strings.settings_section_title_message_shape).uppercase()) { Row(modifier = Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING + 4.dp ) ,verticalAlignment = Alignment.CenterVertically) {
SectionItemViewWithoutMinPadding { Text(stringResource(MR.strings.settings_message_shape_corner), color = colors.onBackground)
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,
onValueChange = { steps = 20,
val diff = it % 0.05f onValueChange = {
appPreferences.chatItemRoundness.set(it + (if (diff >= 0.025f) -diff + 0.05f else -diff)) val diff = it % 0.05f
saveThemeToDatabase(null) appPreferences.chatItemRoundness.set(it + (if (diff >= 0.025f) -diff + 0.05f else -diff))
}, saveThemeToDatabase(null)
Modifier.widthIn(max = (this@BoxWithConstraints.maxWidth - DEFAULT_PADDING * 2) * 0.618f), },
valueRange = 0f..1f, colors = SliderDefaults.colors(
steps = 20, activeTickColor = Color.Transparent,
colors = SliderDefaults.colors( inactiveTickColor = Color.Transparent,
activeTickColor = Color.Transparent,
inactiveTickColor = Color.Transparent,
)
) )
} )
SettingsPreferenceItem(icon = null, stringResource(MR.strings.settings_message_shape_tail), appPreferences.chatItemTail)
} }
SettingsPreferenceItem(icon = null, stringResource(MR.strings.settings_message_shape_tail), appPreferences.chatItemTail)
} }
} }
@@ -205,7 +115,7 @@ object AppearanceScope {
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(50.dp) Box(Modifier.size(60.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 {
@@ -219,7 +129,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(15.dp)) Spacer(Modifier.width(10.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(
@@ -275,7 +185,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, null, null) chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor)
} else { } else {
onDrawBehind { onDrawBehind {
drawRect(themeBackgroundColor) drawRect(themeBackgroundColor)
@@ -604,7 +514,9 @@ 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))
@@ -997,7 +909,10 @@ object AppearanceScope {
currentColors: () -> ThemeManager.ActiveTheme, currentColors: () -> ThemeManager.ActiveTheme,
onColorChange: (Color?) -> Unit, onColorChange: (Color?) -> Unit,
) { ) {
ColumnWithScrollBar(Modifier.imePadding()) { ColumnWithScrollBar(
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 { ColumnWithScrollBar(Modifier.fillMaxWidth()) {
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,10 +22,12 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
@Composable @Composable
fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) -> Unit fun DeveloperView(
m: ChatModel,
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
) { ) {
val m = chatModel ColumnWithScrollBar(Modifier.fillMaxWidth()) {
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
@@ -33,7 +35,7 @@ fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) ->
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)) { ModalManager.start.showModalCloseable { TerminalView(false) } } } ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential), showCustomModal { it, close -> TerminalView(false, close) }) }
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,7 +21,11 @@ fun HelpView(userDisplayName: String) {
@Composable @Composable
fun HelpLayout(userDisplayName: String) { fun HelpLayout(userDisplayName: String) {
ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)){ ColumnWithScrollBar(
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,7 +56,10 @@ 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 { ColumnWithScrollBar(Modifier.fillMaxWidth()) {
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,7 +304,10 @@ 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)) {
@@ -476,7 +479,9 @@ 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,7 +56,9 @@ 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) {
@@ -88,7 +90,9 @@ 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)
} }
@@ -100,7 +104,9 @@ 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,7 +66,9 @@ 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