Compare commits

...

125 Commits

Author SHA1 Message Date
Avently c5f08ad46a old 2024-09-06 23:09:54 +09:00
Avently f2565ef5bf last 2024-09-06 23:06:02 +09:00
Avently d4f56322f7 change 2024-09-06 22:17:31 +09:00
Avently 1a8cb55e70 revert some 2024-09-06 22:11:12 +09:00
Avently 696ddd7a0d Revert "change"
This reverts commit 9693d9e85d.
2024-09-06 22:10:42 +09:00
Avently b25de876e1 change 2024-09-06 22:09:33 +09:00
Avently d96e6314fa Revert "desktop: fix vlc dependency (#4809)"
This reverts commit 087519c39d.
2024-09-06 22:08:30 +09:00
Avently a239dee68f once 2024-09-06 21:49:53 +09:00
Avently 072d547c48 only once 2024-09-06 21:48:53 +09:00
Avently 9693d9e85d change 2024-09-06 21:45:42 +09:00
Avently c78bb5aa5d change 2024-09-06 21:37:50 +09:00
Avently ab5a13081b dispatcher 2024-09-06 21:28:24 +09:00
Diogo 041a2e21d3 initial version of swipable modal 2024-09-06 12:38:54 +01:00
Diogo a02275d28e more android checks 2024-09-06 11:41:15 +01:00
Diogo ce8b23d2ab remove android check 2024-09-06 11:39:59 +01:00
Diogo b77c357704 move bars logic to android 2024-09-06 10:08:43 +01:00
Diogo 103c0d4881 split android and desktop scaffold for picker 2024-09-06 09:45:44 +01:00
Diogo ca1be83db7 paddings 2024-09-05 23:16:13 +01:00
Diogo 7330361289 chevron and gradient 2024-09-05 22:33:52 +01:00
Diogo 6727503803 chevron same size as sun/moon 2024-09-05 18:57:50 +01:00
Diogo fd6a16f3de use bedtime moon for dark mode 2024-09-05 18:50:20 +01:00
Diogo a4db119fcf unread badge same style as chatlist 2024-09-05 18:45:18 +01:00
Diogo c1629ba5d2 remove you 2024-09-05 18:37:56 +01:00
Diogo 0e749a99c9 paddings 2024-09-05 17:12:42 +01:00
Diogo 5806b51d8a desktop paddings 2024-09-05 16:58:25 +01:00
Diogo 27e1c738e5 aligned paddings on desktop 2024-09-05 16:24:55 +01:00
Diogo 3f92e0f249 extra space in the end of user list for android 2024-09-05 14:07:42 +01:00
Diogo fbb776a14a unread badge 2024-09-05 14:04:36 +01:00
Diogo 68f53ff382 centered avatars 2024-09-05 13:57:29 +01:00
Diogo af467bccd3 center android icons with avatar 2024-09-05 13:53:27 +01:00
Diogo 45b82f752a initial desktop design 2024-09-05 13:36:03 +01:00
Diogo 366d043e72 update android space between badges 2024-09-05 13:23:55 +01:00
Diogo 1384cf2470 remove unused var 2024-09-05 12:50:59 +01:00
Diogo 2bea94fe0d fix profile creation in desktop 2024-09-05 12:49:19 +01:00
Diogo 624e49e412 always show your chat profiles in desktop 2024-09-05 11:35:13 +01:00
Diogo 140bc679ab move your chat profiles inside android specific 2024-09-05 11:33:00 +01:00
Diogo 4b61beb237 split inactive user picker between platforms 2024-09-05 11:30:15 +01:00
Diogo 0db7ab964b android paddings 2024-09-05 11:13:18 +01:00
Diogo b993c24be6 gradient 2024-09-05 10:27:20 +01:00
Diogo 3db5c4d3b1 paddings in scroller 2024-09-05 10:15:45 +01:00
Diogo f83768d2ce progress dump on new android design 2024-09-04 23:43:22 +01:00
Diogo 7f28de3b28 close drawer on swipe down 30% 2024-09-04 16:14:48 +01:00
Diogo 1b4aaa0614 move desktop/mobile connection 2024-09-04 15:48:56 +01:00
Diogo 940077a5d3 unused 2024-09-04 15:24:49 +01:00
Diogo 03bf16b97a small refactor 2024-09-04 15:24:32 +01:00
Diogo f99eaf884f remove unused code 2024-09-04 15:17:14 +01:00
Diogo 1adb9ffe79 smaller profile name 2024-09-04 15:14:14 +01:00
Diogo 3968ed2753 fix spacer for row scroll on android 2024-09-04 15:10:04 +01:00
Diogo 69da9532fa bigger profile image smaller gap to name 2024-09-04 14:49:22 +01:00
Diogo 18fa5c75dd close user picker on settings click in desktop 2024-09-04 14:43:01 +01:00
Diogo c3c9adde5c remove shading from desktop 2024-09-04 14:40:10 +01:00
Diogo ff7d1528c4 desktop menu matching size of right side modals 2024-09-04 14:30:33 +01:00
Diogo 5c2b16549f protect android calls 2024-09-04 14:13:25 +01:00
Diogo 249f887531 updated icon color 2024-09-04 14:01:06 +01:00
Diogo 3d9e032900 reset navbar colors on navigate 2024-09-04 13:53:41 +01:00
Diogo 7a92513b34 remove unused var 2024-09-04 13:19:39 +01:00
Diogo 43bc09f9cc change bar and shading on theme change 2024-09-04 13:18:20 +01:00
Diogo 2e27107646 best case bars switching working 2024-09-04 12:28:56 +01:00
Diogo a29ec774e8 improvements on status and nav color set 2024-09-04 12:12:43 +01:00
Diogo 9009744c28 use chevron icon to see all profiles 2024-09-04 11:27:42 +01:00
Diogo 3a1321e4f7 picker should always remain open 2024-09-04 11:22:25 +01:00
Diogo 4a87d85589 align avatars to top 2024-09-04 10:53:59 +01:00
Diogo ae58948ee8 separate title 2024-09-04 10:29:38 +01:00
Diogo f9778546dd gradient and horizontal scroll 2024-09-04 10:25:00 +01:00
Avently a9b24d18be color mix 2024-09-04 03:03:40 +09:00
Diogo d119477d5c broken transition change 2024-09-03 17:10:14 +01:00
Diogo bebae7209b simplify nav bar color set 2024-09-03 14:59:13 +01:00
Diogo 1ddd6a8c18 Merge branch 'master' into dc/deskto-android-new-user-picker 2024-09-03 14:51:11 +01:00
Diogo aae79ffa5f same button as ios for opening all profiles 2024-09-03 11:16:17 +01:00
Diogo 2868005d2f Merge branch 'master' into dc/deskto-android-new-user-picker 2024-09-03 10:47:47 +01:00
Diogo bd8cc1040d stop pushing shade up 2024-09-02 11:08:16 +01:00
Diogo b878fd21b3 shading colors 2024-09-02 10:25:17 +01:00
Diogo ac84e1da64 add system mode toast 2024-08-31 23:32:41 +01:00
Diogo 996d92b313 fade in fade out 2024-08-31 23:26:16 +01:00
Diogo 3329544649 animate shading 2024-08-31 23:22:04 +01:00
Diogo d663cc010a remove drawer unused code 2024-08-31 23:08:05 +01:00
Diogo a278432f5c bar colors 2024-08-31 23:04:28 +01:00
Diogo 894a9873cb remove logs 2024-08-31 22:20:29 +01:00
Diogo 56789de01e fix use from desktop 2024-08-31 22:19:14 +01:00
Diogo e08b9f69e9 center settings 2024-08-31 15:20:56 +01:00
Diogo 67ca556331 settings as modal 2024-08-31 15:12:46 +01:00
Diogo ed13bc9f27 icons 2024-08-31 14:39:11 +01:00
Diogo d098f1c5cc icon colors 2024-08-31 13:47:20 +01:00
Evgeny Poberezkin 95062075f0 Merge branch 'master' into dc/deskto-android-new-user-picker 2024-08-31 11:59:30 +01:00
Diogo 8fe4be3c0c h2 is too big 2024-08-31 08:43:01 +01:00
Diogo 1e37e77157 profile name as header 2024-08-31 08:41:07 +01:00
Diogo e609de18c5 padding 2024-08-31 08:38:28 +01:00
Diogo 2f97fa6b48 add icon for dark mode 2024-08-31 08:32:49 +01:00
Diogo f7ab32bdf8 font match 2024-08-31 08:25:54 +01:00
Diogo 1ab692a5e8 create public address 2024-08-30 23:18:59 +01:00
Diogo 4f4713e5af better switch of opacity bg 2024-08-30 23:11:33 +01:00
Diogo 421e2b34f6 simplex address -> public 2024-08-30 23:09:40 +01:00
Diogo 0365485591 device pill in app bar 2024-08-30 23:05:09 +01:00
Diogo 5d88ef08eb hosts ordering and fixes 2024-08-30 22:51:11 +01:00
Diogo e74aa5b5ea bigger space between profiles 2024-08-30 22:45:19 +01:00
Diogo b6fea51f69 change view all profiles icon 2024-08-30 22:42:24 +01:00
Diogo 50b86f7dff pills order 2024-08-30 22:22:21 +01:00
Diogo bfbe3cf062 picker positioning 2024-08-30 22:15:29 +01:00
Diogo fd1cf5f1ed hide non active devices from pills 2024-08-30 22:05:30 +01:00
Diogo bd8c91585f padding 2024-08-30 22:03:54 +01:00
Diogo 9f1f54d9e0 active is grey 2024-08-30 21:54:28 +01:00
Diogo 82ce157c65 address messaging 2024-08-30 21:51:42 +01:00
Diogo a293103ee9 Merge branch 'master' into dc/deskto-android-new-user-picker 2024-08-30 21:41:59 +01:00
Diogo 32bd3c941a simple animations and shadow 2024-08-30 17:24:53 +01:00
Diogo 4907259d2e fonts and align 2024-08-30 16:41:03 +01:00
Diogo a40dd2591b handle big names 2024-08-30 16:14:25 +01:00
Diogo f8072e057c paddings 2024-08-30 15:34:28 +01:00
Diogo 9a6a03ffc3 remove create profile from settings 2024-08-30 15:10:41 +01:00
Diogo 9ea6b4d595 picker should now open for single user 2024-08-30 14:52:09 +01:00
Diogo 019e1e1653 safe remove of you settings section 2024-08-30 14:45:54 +01:00
Diogo eddb2e250b chat database settings section 2024-08-30 14:42:08 +01:00
Diogo 27a80cb66c add unread badge to users row 2024-08-30 14:34:03 +01:00
Diogo 4f9aabce7d color changes 2024-08-30 14:29:54 +01:00
Diogo 712aade625 remove stroke 2024-08-30 14:11:20 +01:00
Diogo 46ce478676 profile picker modal for external shares 2024-08-30 13:32:44 +01:00
Diogo b78512031e icon buttons 2024-08-30 12:27:14 +01:00
Diogo 3c6eb3899f remote hosts working 2024-08-30 10:12:26 +01:00
Diogo ea01373c1a Merge branch 'master' into dc/deskto-android-new-user-picker 2024-08-30 09:43:55 +01:00
Diogo a1325a7695 cleanup 2024-08-29 17:20:30 +01:00
Diogo 48398c2119 close picker on desktop opening modals 2024-08-29 17:05:58 +01:00
Diogo ff8e55599b color mode switcher 2024-08-29 16:13:16 +01:00
Diogo 0cd1208daa android dirty layout 2024-08-29 15:16:10 +01:00
Diogo 2686544723 Merge branch 'master' into dc/deskto-android-new-user-picker 2024-08-29 13:42:26 +01:00
Diogo f7a21f9792 Merge branch 'master' into dc/deskto-android-new-user-picker 2024-08-29 11:36:12 +01:00
Diogo 96aa960bee user picker as modal 2024-08-28 10:59:24 +01:00
28 changed files with 936 additions and 392 deletions
@@ -1,5 +1,7 @@
package chat.simplex.app package chat.simplex.app
import android.animation.ArgbEvaluator
import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.* import android.app.*
import android.content.Context import android.content.Context
@@ -8,6 +10,8 @@ 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 android.view.View
import androidx.compose.animation.core.*
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -260,7 +264,6 @@ class SimplexApp: Application(), LifecycleEventObserver {
override fun androidSetNightModeIfSupported() { override fun androidSetNightModeIfSupported() {
if (Build.VERSION.SDK_INT < 31) return if (Build.VERSION.SDK_INT < 31) return
val light = if (CurrentColors.value.name == DefaultTheme.SYSTEM_THEME_NAME) { val light = if (CurrentColors.value.name == DefaultTheme.SYSTEM_THEME_NAME) {
null null
} else { } else {
@@ -275,6 +278,31 @@ class SimplexApp: Application(), LifecycleEventObserver {
uiModeManager.setApplicationNightMode(mode) uiModeManager.setApplicationNightMode(mode)
} }
override fun androidSetDrawerStatusAndNavBarColor(
isLight: Boolean,
drawerShadingColor: Animatable<Color, AnimationVector4D>,
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
window.statusBarColor = baseBackgroundColor.mixWith(drawerShadingColor.value.copy(1f), 1 - drawerShadingColor.value.alpha).toArgb()
val navBar = navBarColor.toArgb()
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) { 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")
+1 -1
View File
@@ -102,7 +102,7 @@ kotlin {
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.8") implementation("com.github.Dansoftowner:jSystemThemeDetector:3.8")
implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT") implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT")
implementation("org.slf4j:slf4j-simple:2.0.12") implementation("org.slf4j:slf4j-simple:2.0.12")
implementation("uk.co.caprica:vlcj:4.8.3") implementation("uk.co.caprica:vlcj:4.8.2")
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf85a") implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf85a")
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf85a") implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf85a")
implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("com.squareup.okhttp3:okhttp:4.12.0")
@@ -0,0 +1,187 @@
package chat.simplex.common.views.chatlist
import SectionItemView
import androidx.compose.animation.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.DrawerDefaults.ScrimOpacity
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.User
import chat.simplex.common.model.UserInfo
import chat.simplex.common.platform.appPlatform
import chat.simplex.common.platform.platform
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@Composable
actual fun UserPickerInactiveUsersSection(
users: List<UserInfo>,
stopped: Boolean,
onShowAllProfilesClicked: () -> Unit,
onUserClicked: (user: User) -> Unit,
) {
val scrollState = rememberScrollState()
if (users.isNotEmpty()) {
SectionItemView(padding = PaddingValues(start = 16.dp, top = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL, bottom = DEFAULT_PADDING_HALF), disabled = stopped) {
Box {
Row(
modifier = Modifier.padding(end = DEFAULT_PADDING + 30.dp).horizontalScroll(scrollState)
) {
users.forEach { u ->
UserPickerInactiveUserBadge(u, stopped) {
onUserClicked(it)
}
Spacer(Modifier.width(20.dp))
}
Spacer(Modifier.width(60.dp))
}
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.fillMaxWidth()
.padding(end = DEFAULT_PADDING + 30.dp)
.height(60.dp)
) {
Canvas(modifier = Modifier.size(60.dp)) {
drawRect(
brush = Brush.horizontalGradient(
colors = listOf(
Color.Transparent,
CurrentColors.value.colors.surface,
)
),
)
}
}
Row(
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.height(60.dp)
.fillMaxWidth()
.padding(end = DEFAULT_PADDING)
) {
IconButton(
onClick = onShowAllProfilesClicked,
enabled = !stopped
) {
Icon(
painterResource(MR.images.ic_chevron_right),
stringResource(MR.strings.your_chat_profiles),
tint = MaterialTheme.colors.secondary,
modifier = Modifier.size(34.dp)
)
}
}
}
}
} else {
UserPickerOptionRow(
painterResource(MR.images.ic_manage_accounts),
stringResource(MR.strings.your_chat_profiles),
onShowAllProfilesClicked
)
}
}
@Composable
actual fun UserPickerScaffold(pickerState: MutableStateFlow<AnimatedViewState>, content: @Composable () -> Unit) {
val currentTheme by CurrentColors.collectAsState()
val resultingColor by remember {
derivedStateOf {
if (currentTheme.colors.isLight) currentTheme.colors.onSurface.copy(alpha = ScrimOpacity) else Color.Black.copy(0.64f)
}
}
val animatedColor = remember {
androidx.compose.animation.core.Animatable(
if (pickerState.value.isVisible()) Color.Transparent else resultingColor,
Color.VectorConverter(resultingColor.colorSpace)
)
}
LaunchedEffect(Unit) {
launch {
snapshotFlow { ModalManager.start.modalCount.value }
.collect { modalCount ->
val colors = CurrentColors.value.colors
if (modalCount == 0 && pickerState.value.isVisible()) {
platform.androidSetDrawerStatusAndNavBarColor(
isLight = colors.isLight,
drawerShadingColor = animatedColor,
toolbarOnTop = !appPrefs.oneHandUI.get(),
navBarColor = colors.surface
)
}
}
}
}
LaunchedEffect(Unit) {
snapshotFlow { currentTheme }
.distinctUntilChanged()
.collect {
launch {
pickerState.collect {
val newState = it
val colors = CurrentColors.value.colors
val toColor = if (colors.isLight) colors.onSurface.copy(alpha = ScrimOpacity) else Color.Black.copy(0.64f)
animatedColor.animateTo(if (newState.isVisible()) toColor else Color.Transparent, newChatSheetAnimSpec()) {
if (newState.isVisible()) {
platform.androidSetDrawerStatusAndNavBarColor(
isLight = colors.isLight,
drawerShadingColor = animatedColor,
toolbarOnTop = !appPrefs.oneHandUI.get(),
navBarColor = colors.surface
)
} else if (newState.isHiding()) {
platform.androidSetDrawerStatusAndNavBarColor(
isLight = colors.isLight,
drawerShadingColor = animatedColor,
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
})
)
}
}
}
}
}
}
Box(Modifier.drawBehind { drawRect(animatedColor.value) }) {
AnimatedVisibility(
visible = pickerState.run { value.isVisible() },
enter = slideInVertically(
initialOffsetY = { it },
animationSpec = userPickerAnimSpec()
) + fadeIn(animationSpec = userPickerAnimSpec()),
exit = slideOutVertically(
targetOffsetY = { it },
animationSpec = userPickerAnimSpec()
) + fadeOut(animationSpec = userPickerAnimSpec())
) {
content()
}
}
}
@@ -46,7 +46,6 @@ import kotlin.math.sqrt
data class SettingsViewState( data class SettingsViewState(
val userPickerState: MutableStateFlow<AnimatedViewState>, val userPickerState: MutableStateFlow<AnimatedViewState>,
val scaffoldState: ScaffoldState
) )
@Composable @Composable
@@ -145,8 +144,7 @@ fun MainScreen() {
userPickerState.value = AnimatedViewState.VISIBLE userPickerState.value = AnimatedViewState.VISIBLE
} }
} }
val scaffoldState = rememberScaffoldState() val settingsState = remember { SettingsViewState(userPickerState) }
val settingsState = remember { SettingsViewState(userPickerState, scaffoldState) }
SetupClipboardListener() SetupClipboardListener()
if (appPlatform.isAndroid) { if (appPlatform.isAndroid) {
AndroidScreen(settingsState) AndroidScreen(settingsState)
@@ -369,7 +367,6 @@ fun EndPartOfScreen() {
@Composable @Composable
fun DesktopScreen(settingsState: SettingsViewState) { fun DesktopScreen(settingsState: SettingsViewState) {
Box { Box {
// 56.dp is a size of unused space of settings drawer
Box(Modifier.width(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier + 56.dp)) { Box(Modifier.width(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier + 56.dp)) {
StartPartOfScreen(settingsState) StartPartOfScreen(settingsState)
} }
@@ -388,25 +385,21 @@ fun DesktopScreen(settingsState: SettingsViewState) {
EndPartOfScreen() EndPartOfScreen()
} }
} }
val (userPickerState, scaffoldState ) = settingsState val (userPickerState ) = settingsState
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
if (scaffoldState.drawerState.isOpen || (ModalManager.start.hasModalsOpen && !ModalManager.center.hasModalsOpen)) { if (ModalManager.start.hasModalsOpen && !ModalManager.center.hasModalsOpen) {
Box( Box(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) .padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = {
ModalManager.start.closeModals() ModalManager.start.closeModals()
scope.launch { settingsState.scaffoldState.drawerState.close() }
}) })
) )
} }
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier))
tryOrShowError("UserPicker", error = {}) { tryOrShowError("UserPicker", error = {}) {
UserPicker(chatModel, userPickerState) { UserPicker(chatModel, userPickerState, setPerformLA = AppLock::setPerformLA)
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
userPickerState.value = AnimatedViewState.GONE
}
} }
ModalManager.fullscreen.showInView() ModalManager.fullscreen.showInView()
} }
@@ -414,7 +414,7 @@ object ChatModel {
// We delete taking into account meta.createdAt to make sure we will not be in situation when two items with the same id will be deleted // We delete taking into account meta.createdAt to make sure we will not be in situation when two items with the same id will be deleted
// (it can happen if already deleted chat item in backend still in the list and new one came with the same (re-used) chat item id) // (it can happen if already deleted chat item in backend still in the list and new one came with the same (re-used) chat item id)
val remove = it.id == cItem.id && it.meta.createdAt == cItem.meta.createdAt val remove = it.id == cItem.id && it.meta.createdAt == cItem.meta.createdAt
if (remove) { AudioPlayer.stop(it) } if (remove) { withBGApi { AudioPlayer.stop(it) } }
remove remove
} }
} }
@@ -1,7 +1,6 @@
package chat.simplex.common.platform package chat.simplex.common.platform
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.*
import androidx.compose.animation.core.AnimationVector1D
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.*
@@ -22,6 +21,7 @@ interface PlatformInterface {
fun androidIsBackgroundCallAllowed(): Boolean = true fun androidIsBackgroundCallAllowed(): Boolean = true
fun androidSetNightModeIfSupported() {} fun androidSetNightModeIfSupported() {}
fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {} fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {}
fun androidSetDrawerStatusAndNavBarColor(isLight: Boolean, drawerShadingColor: Animatable<Color, AnimationVector4D>, 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() {}
@@ -28,7 +28,7 @@ data class CurrentlyPlayingState(
interface AudioPlayerInterface { interface AudioPlayerInterface {
val currentlyPlaying: MutableState<CurrentlyPlayingState?> val currentlyPlaying: MutableState<CurrentlyPlayingState?>
fun play( suspend fun play(
fileSource: CryptoFile, fileSource: CryptoFile,
audioPlaying: MutableState<Boolean>, audioPlaying: MutableState<Boolean>,
progress: MutableState<Int>, progress: MutableState<Int>,
@@ -36,12 +36,12 @@ interface AudioPlayerInterface {
resetOnEnd: Boolean, resetOnEnd: Boolean,
smallView: Boolean, smallView: Boolean,
) )
fun stop() suspend fun stop()
fun stop(item: ChatItem) suspend fun stop(item: ChatItem)
fun stop(fileName: String?) suspend fun stop(fileName: String?)
fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>) suspend fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>)
fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) suspend fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?)
fun duration(unencryptedFilePath: String): Int? suspend fun duration(unencryptedFilePath: String): Int?
} }
expect object AudioPlayer: AudioPlayerInterface expect object AudioPlayer: AudioPlayerInterface
@@ -109,7 +109,6 @@ fun TerminalLayout(
} }
}, },
contentColor = LocalContentColor.current, contentColor = LocalContentColor.current,
drawerContentColor = LocalContentColor.current,
modifier = Modifier.navigationBarsWithImePadding() modifier = Modifier.navigationBarsWithImePadding()
) { contentPadding -> ) { contentPadding ->
Surface( Surface(
@@ -178,7 +178,9 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
selectedChatItems = selectedChatItems, selectedChatItems = selectedChatItems,
back = { back = {
hideKeyboard(view) hideKeyboard(view)
AudioPlayer.stop() withBGApi {
AudioPlayer.stop()
}
chatModel.chatId.value = null chatModel.chatId.value = null
chatModel.groupMembers.clear() chatModel.groupMembers.clear()
chatModel.groupMembersIndexes.clear() chatModel.groupMembersIndexes.clear()
@@ -654,7 +656,6 @@ fun ChatLayout(
modifier = Modifier.navigationBarsWithImePadding(), modifier = Modifier.navigationBarsWithImePadding(),
floatingActionButton = { floatingButton.value() }, floatingActionButton = { floatingButton.value() },
contentColor = LocalContentColor.current, contentColor = LocalContentColor.current,
drawerContentColor = LocalContentColor.current,
backgroundColor = Color.Unspecified backgroundColor = Color.Unspecified
) { contentPadding -> ) { contentPadding ->
val wallpaperImage = MaterialTheme.wallpaper.type.image val wallpaperImage = MaterialTheme.wallpaper.type.image
@@ -52,10 +52,12 @@ fun ComposeVoiceView(
) { ) {
IconButton( IconButton(
onClick = { onClick = {
if (!audioPlaying.value) { withBGApi {
AudioPlayer.play(CryptoFile.plain(filePath), audioPlaying, progress, duration, resetOnEnd = false, smallView = false) if (!audioPlaying.value) {
} else { AudioPlayer.play(CryptoFile.plain(filePath), audioPlaying, progress, duration, resetOnEnd = false, smallView = false)
AudioPlayer.pause(audioPlaying, progress) } else {
AudioPlayer.pause(audioPlaying, progress)
}
} }
}, },
enabled = finishedRecording enabled = finishedRecording
@@ -87,7 +89,9 @@ fun ComposeVoiceView(
if (cancelEnabled) { if (cancelEnabled) {
IconButton( IconButton(
onClick = { onClick = {
AudioPlayer.stop(filePath) withBGApi {
AudioPlayer.stop(filePath)
}
cancelVoice() cancelVoice()
}, },
modifier = Modifier.padding(0.dp) modifier = Modifier.padding(0.dp)
@@ -121,7 +125,7 @@ fun FinishedRecordingSlider(backgroundColor: Color, progress: MutableState<Int>,
0.24f) 0.24f)
Slider( Slider(
progress.value.toFloat(), progress.value.toFloat(),
onValueChange = { AudioPlayer.seekTo(it.toInt(), progress, filePath) }, onValueChange = { withBGApi { AudioPlayer.seekTo(it.toInt(), progress, filePath) } },
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.drawBehind { .drawBehind {
@@ -62,8 +62,10 @@ fun CIVoiceView(
val play: () -> Unit = { val play: () -> Unit = {
val playIfExists = { val playIfExists = {
if (fileSource.value != null) { if (fileSource.value != null) {
AudioPlayer.play(fileSource.value!!, audioPlaying, progress, duration, resetOnEnd = true, smallView = smallView) withBGApi {
brokenAudio = !audioPlaying.value AudioPlayer.play(fileSource.value!!, audioPlaying, progress, duration, resetOnEnd = true, smallView = smallView)
brokenAudio = !audioPlaying.value
}
} }
} }
if (chatModel.connectedToRemote() && fileSource.value == null) { if (chatModel.connectedToRemote() && fileSource.value == null) {
@@ -75,7 +77,10 @@ fun CIVoiceView(
} else playIfExists() } else playIfExists()
} }
val pause = { val pause = {
AudioPlayer.pause(audioPlaying, progress) withBGApi {
AudioPlayer.pause(audioPlaying, progress)
}
Unit
} }
val text = remember(ci.file?.fileId, ci.file?.fileStatus) { val text = remember(ci.file?.fileId, ci.file?.fileStatus) {
derivedStateOf { derivedStateOf {
@@ -87,7 +92,9 @@ fun CIVoiceView(
} }
} }
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, sizeMultiplier, play, pause, longClick, receiveFile) { VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, sizeMultiplier, play, pause, longClick, receiveFile) {
AudioPlayer.seekTo(it, progress, fileSource.value?.filePath) withBGApi {
AudioPlayer.seekTo(it, progress, fileSource.value?.filePath)
}
} }
if (smallView) { if (smallView) {
KeyChangeEffect(chatModel.chatId.value, chatModel.currentUser.value?.userId, chatModel.currentRemoteHost.value) { KeyChangeEffect(chatModel.chatId.value, chatModel.currentUser.value?.userId, chatModel.currentRemoteHost.value) {
@@ -23,6 +23,7 @@ import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import chat.simplex.common.AppLock
import chat.simplex.common.SettingsViewState import chat.simplex.common.SettingsViewState
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.appPrefs
@@ -155,16 +156,15 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
} }
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
val scope = rememberCoroutineScope() val (userPickerState) = settingsState
val (userPickerState, scaffoldState ) = settingsState
Scaffold( Scaffold(
topBar = { topBar = {
if (!oneHandUI.value) { if (!oneHandUI.value) {
Column(Modifier.padding(end = endPadding)) { Column(Modifier.padding(end = endPadding)) {
ChatListToolbar( ChatListToolbar(
scaffoldState.drawerState,
userPickerState, userPickerState,
stopped, stopped,
setPerformLA,
) )
Divider() Divider()
} }
@@ -175,30 +175,14 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
Column(Modifier.padding(end = endPadding)) { Column(Modifier.padding(end = endPadding)) {
Divider() Divider()
ChatListToolbar( ChatListToolbar(
scaffoldState.drawerState,
userPickerState, userPickerState,
stopped, stopped,
) setPerformLA,
} )
}
},
scaffoldState = scaffoldState,
drawerContent = {
tryOrShowError("Settings", error = { ErrorSettingsView() }) {
val handler = remember { AppBarHandler() }
CompositionLocalProvider(
LocalAppBarHandler provides handler
) {
ModalView(showClose = appPlatform.isDesktop, close = { scope.launch { scaffoldState.drawerState.close() } }) {
SettingsView(chatModel, setPerformLA, scaffoldState.drawerState)
}
} }
} }
}, },
contentColor = LocalContentColor.current, contentColor = LocalContentColor.current,
drawerContentColor = LocalContentColor.current,
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
drawerGesturesEnabled = appPlatform.isAndroid,
floatingActionButton = { floatingActionButton = {
if (!oneHandUI.value && searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) { if (!oneHandUI.value && searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
FloatingActionButton( FloatingActionButton(
@@ -252,11 +236,8 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
UserPicker( UserPicker(
chatModel = chatModel, chatModel = chatModel,
userPickerState = userPickerState, userPickerState = userPickerState,
contentAlignment = if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart setPerformLA = AppLock::setPerformLA
) { )
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
userPickerState.value = AnimatedViewState.GONE
}
} }
} }
} }
@@ -278,7 +259,7 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
} }
@Composable @Composable
private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean) { 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
@@ -349,18 +330,18 @@ private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableSt
DefaultTopAppBar( DefaultTopAppBar(
navigationButton = { navigationButton = {
if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) { if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) {
NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } } NavigationButtonMenu {
ModalManager.start.showModalCloseable { close ->
SettingsView(chatModel, setPerformLA, close)
}
}
} else { } else {
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 allRead = users val allRead = users
.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) {
if (users.size == 1 && chatModel.remoteHosts.isEmpty()) {
scope.launch { drawerState.open() }
} else {
userPickerState.value = AnimatedViewState.VISIBLE userPickerState.value = AnimatedViewState.VISIBLE
}
} }
} }
}, },
@@ -23,19 +23,17 @@ import kotlinx.coroutines.flow.MutableStateFlow
@Composable @Composable
fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stopped: Boolean) { fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stopped: Boolean) {
var searchInList by rememberSaveable { mutableStateOf("") } var searchInList by rememberSaveable { mutableStateOf("") }
val (userPickerState, scaffoldState) = settingsState val (userPickerState) = settingsState
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
val oneHandUI = remember { appPrefs.oneHandUI.state } val oneHandUI = remember { appPrefs.oneHandUI.state }
Scaffold( Scaffold(
Modifier.padding(end = endPadding), Modifier.padding(end = endPadding),
contentColor = LocalContentColor.current, contentColor = LocalContentColor.current,
drawerContentColor = LocalContentColor.current,
scaffoldState = scaffoldState,
topBar = { topBar = {
if (!oneHandUI.value) { if (!oneHandUI.value) {
Column { Column {
ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } ShareListToolbar(chatModel, stopped) { searchInList = it.trim() }
Divider() Divider()
} }
} }
@@ -44,7 +42,7 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
if (oneHandUI.value) { if (oneHandUI.value) {
Column { Column {
Divider() Divider()
ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } ShareListToolbar(chatModel, stopped) { searchInList = it.trim() }
} }
} }
} }
@@ -92,21 +90,6 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
} }
} }
} }
if (appPlatform.isAndroid) {
tryOrShowError("UserPicker", error = {}) {
UserPicker(
chatModel,
userPickerState,
showSettings = false,
showCancel = true,
contentAlignment = if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart,
cancelClicked = {
chatModel.sharedContent.value = null
userPickerState.value = AnimatedViewState.GONE
}
)
}
}
} }
private fun hasSimplexLink(msg: String): Boolean { private fun hasSimplexLink(msg: String): Boolean {
@@ -122,7 +105,7 @@ private fun EmptyList() {
} }
@Composable @Composable
private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, onSearchValueChanged: (String) -> Unit) { private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
var showSearch by rememberSaveable { mutableStateOf(false) } var showSearch by rememberSaveable { mutableStateOf(false) }
val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false } val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false }
if (showSearch) { if (showSearch) {
@@ -138,7 +121,24 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
.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) {
userPickerState.value = AnimatedViewState.VISIBLE if (appPlatform.isAndroid) {
ModalManager.start.showCustomModal { close ->
val search = rememberSaveable { mutableStateOf("") }
ModalView(
{ close() },
endButtons = {
SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
},
content = {
ShareListUserPicker(
chatModel = chatModel,
search = search,
close = close,
)
}
)
}
}
} }
} }
else -> NavigationButtonBack(onButtonClicked = { else -> NavigationButtonBack(onButtonClicked = {
@@ -1,24 +1,36 @@
package chat.simplex.common.views.chatlist package chat.simplex.common.views.chatlist
import SectionItemView import SectionItemView
import SectionView
import TextIconSpaced
import androidx.compose.animation.*
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectVerticalDragGestures
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.* import androidx.compose.foundation.shape.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.DrawerDefaults.ScrimOpacity
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.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.TextStyle
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextAlign
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.appPrefs
@@ -28,26 +40,289 @@ import chat.simplex.common.ui.theme.*
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.views.CreateProfile import chat.simplex.common.views.CreateProfile
import chat.simplex.common.views.chat.ComposeContextItem
import chat.simplex.common.views.chat.ComposeState
import chat.simplex.common.views.newchat.*
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.common.views.remote.* import chat.simplex.common.views.remote.*
import chat.simplex.common.views.usersettings.doWithAuth import chat.simplex.common.views.usersettings.*
import chat.simplex.common.views.usersettings.AppearanceScope.ColorModeSwitcher
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
@Composable fun UserPickerOptionRow(icon: Painter, text: String, click: (() -> Unit)? = null, disabled: Boolean = false) {
SectionItemView(click, disabled = disabled, extraPadding = true) {
Icon(icon, text, tint = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.secondary)
TextIconSpaced()
Text(text = text, color = if (disabled) MaterialTheme.colors.secondary else Color.Unspecified)
}
}
@Composable
fun ShareListUserPicker(
chatModel: ChatModel,
search: MutableState<String>,
close: () -> Unit,
) {
val switchingProfile = remember { mutableStateOf(false) }
val searchTextOrPassword = rememberSaveable { search }
val profiles = remember {
chatModel.users.map { it.user }.sortedBy { !it.activeUser }
}
val filteredProfiles by remember {
derivedStateOf { filteredProfiles(profiles, searchTextOrPassword.value) }
}
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(switchingProfile.value) {
progressByTimeout = if (switchingProfile.value) {
delay(500)
switchingProfile.value
} else {
false
}
}
Box {
Column(
Modifier
.fillMaxSize()
.alpha(if (progressByTimeout) 0.6f else 1f)
) {
LazyColumnWithScrollBar(userScrollEnabled = !switchingProfile.value) {
item {
AppBarTitle(stringResource(MR.strings.select_chat_profile), hostDevice(chatModel.remoteHostId), bottomPadding = DEFAULT_PADDING)
}
itemsIndexed(filteredProfiles) { _, p ->
ProfilePickerOption(
title = p.chatViewName,
disabled = switchingProfile.value || p.activeUser,
image = { ProfileImage(size = 42.dp, image = p.image) },
selected = p.activeUser,
onSelected = {
switchingProfile.value = true
withApi {
try {
controller.changeActiveUser(
rhId = p.remoteHostId,
toUserId = p.userId,
viewPwd = if (p.hidden) searchTextOrPassword.value else null
)
if (chatModel.currentUser.value?.userId != p.userId) {
AlertManager.shared.showAlertMsg(
generalGetString(
MR.strings.switching_profile_error_title
),
String.format(generalGetString(MR.strings.switching_profile_error_message), p.chatViewName)
)
}
close.invoke()
} finally {
switchingProfile.value = false
}
}
}
)
}
}
}
if (progressByTimeout) {
DefaultProgressView("")
}
}
}
@Composable
private fun ActiveUserSection(
chatModel: ChatModel,
userPickerState: MutableStateFlow<AnimatedViewState>,
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
) {
val currentUser = remember { chatModel.currentUser }.value
val stopped = chatModel.chatRunning.value == false
if (currentUser != null) {
SectionView {
SectionItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) {
ProfilePreview(currentUser.profile, stopped = stopped)
}
UserPickerOptionRow(
painterResource(MR.images.ic_qr_code),
if (chatModel.userAddress.value != null) generalGetString(MR.strings.your_public_contact_address) else generalGetString(MR.strings.create_public_contact_address),
showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped
)
UserPickerOptionRow(
painterResource(MR.images.ic_toggle_on),
stringResource(MR.strings.chat_preferences),
click = if (stopped) null else ({
showCustomModal { m, close ->
PreferencesView(m, m.currentUser.value ?: return@showCustomModal, close)
}()
}),
disabled = stopped
)
}
} else {
SectionView {
if (chatModel.desktopNoUserNoRemote) {
UserPickerOptionRow(
painterResource(MR.images.ic_manage_accounts),
generalGetString(MR.strings.create_chat_profile),
{
doWithAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) {
ModalManager.center.showModalCloseable { close ->
LaunchedEffect(Unit) {
userPickerState.value = AnimatedViewState.HIDING
}
CreateProfile(chat.simplex.common.platform.chatModel, close)
}
}
}
)
}
}
}
}
@Composable
private fun BoxScope.unreadBadge(unreadCount: Int) {
Text(
if (unreadCount > 0) unreadCountStr(unreadCount) else "",
color = Color.White,
fontSize = 10.sp,
style = TextStyle(textAlign = TextAlign.Center),
modifier = Modifier
.offset(y = 3.sp.toDp())
.background(MaterialTheme.colors.primaryVariant, shape = CircleShape)
.badgeLayout()
.padding(horizontal = 2.sp.toDp())
.padding(vertical = 2.sp.toDp())
.align(Alignment.TopEnd)
)
}
@Composable
fun UserPickerInactiveUserBadge(userInfo: UserInfo, stopped: Boolean, size: Int = 60, onClick: (user: User) -> Unit) {
Box {
IconButton(
onClick = { onClick(userInfo.user) },
enabled = !stopped
) {
Box {
ProfileImage(size = size.dp, image = userInfo.user.profile.image, color = MaterialTheme.colors.secondaryVariant)
if (userInfo.unreadCount > 0) {
unreadBadge(userInfo.unreadCount)
}
}
}
}
}
@Composable
fun DraggableBottomDrawerModifier(
state: DismissState,
swipeDistance: Float,
): Modifier {
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val anchors = mutableMapOf(0f to DismissValue.Default)
anchors += swipeDistance to DismissValue.DismissedToEnd
val offsetY = state.offset.value.roundToInt();
return Modifier.swipeable(
state = state,
anchors = anchors,
thresholds = { _, _ -> FractionalThreshold(0.99f) },
orientation = Orientation.Vertical,
reverseDirection = isRtl,
).offset { IntOffset(0, if (offsetY > 0) offsetY else 0) }
}
@Composable
private fun GlobalSettingsSection(
chatModel: ChatModel,
userPickerState: MutableStateFlow<AnimatedViewState>,
setPerformLA: (Boolean) -> Unit,
onUserClicked: (user: User) -> Unit,
onShowAllProfilesClicked: () -> Unit
) {
val stopped = chatModel.chatRunning.value == false
val users by remember {
derivedStateOf {
chatModel.users
.filter { u -> !u.user.hidden && !u.user.activeUser }
}
}
SectionView {
UserPickerInactiveUsersSection(
users = users,
onShowAllProfilesClicked = onShowAllProfilesClicked,
onUserClicked = onUserClicked,
stopped = stopped
)
if (appPlatform.isAndroid) {
val text = generalGetString(MR.strings.settings_section_title_use_from_desktop).lowercase().capitalize(Locale.current)
UserPickerOptionRow(
painterResource(MR.images.ic_desktop),
text,
click = {
ModalManager.start.showCustomModal { close ->
ConnectDesktopView(close)
}
}
)
} else {
UserPickerOptionRow(
icon = painterResource(MR.images.ic_smartphone_300),
text = stringResource(if (remember { chat.simplex.common.platform.chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles),
click = {
userPickerState.value = AnimatedViewState.HIDING
ModalManager.start.showModal {
ConnectMobileView()
}
},
disabled = stopped
)
}
SectionItemView(
click = {
if (appPlatform.isDesktop) {
userPickerState.value = AnimatedViewState.HIDING
}
ModalManager.start.showModalCloseable { close ->
SettingsView(chatModel, setPerformLA, close)
}
},
padding = PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING + 2.dp)
) {
val text = generalGetString(MR.strings.settings_section_title_settings).lowercase().capitalize(Locale.current)
Icon(painterResource(MR.images.ic_settings), text, tint = MaterialTheme.colors.secondary)
TextIconSpaced()
Text(text, color = Color.Unspecified)
Spacer(Modifier.weight(1f))
ColorModeSwitcher()
}
}
}
@Composable @Composable
fun UserPicker( fun UserPicker(
chatModel: ChatModel, chatModel: ChatModel,
userPickerState: MutableStateFlow<AnimatedViewState>, userPickerState: MutableStateFlow<AnimatedViewState>,
showSettings: Boolean = true, setPerformLA: (Boolean) -> Unit,
contentAlignment: Alignment = Alignment.TopStart,
showCancel: Boolean = false,
cancelClicked: () -> Unit = {},
useFromDesktopClicked: () -> Unit = {},
settingsClicked: () -> Unit = {},
) { ) {
val scope = rememberCoroutineScope()
var newChat by remember { mutableStateOf(userPickerState.value) } var newChat by remember { mutableStateOf(userPickerState.value) }
if (newChat.isVisible()) { if (newChat.isVisible()) {
BackHandler { BackHandler {
@@ -68,6 +343,7 @@ fun UserPicker(
} }
} }
val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) } val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
launch { launch {
userPickerState.collect { userPickerState.collect {
@@ -79,6 +355,7 @@ fun UserPicker(
} }
} }
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
snapshotFlow { newChat.isVisible() } snapshotFlow { newChat.isVisible() }
.distinctUntilChanged() .distinctUntilChanged()
@@ -124,128 +401,114 @@ fun UserPicker(
} }
} }
} }
val UsersView: @Composable ColumnScope.() -> Unit = {
users.forEach { u -> UserPickerScaffold(pickerState = userPickerState) {
UserProfilePickerItem(u.user, u.unreadCount, openSettings = settingsClicked) { Box(
userPickerState.value = AnimatedViewState.HIDING
if (!u.user.activeUser) {
withBGApi {
controller.showProgressIfNeeded {
ModalManager.closeAllModalsEverywhere()
chatModel.controller.changeActiveUser(u.user.remoteHostId, u.user.userId, null)
}
}
}
}
Divider(Modifier.requiredHeight(1.dp))
if (u.user.activeUser) Divider(Modifier.requiredHeight(0.5.dp))
}
}
val xOffset = with(LocalDensity.current) { 10.dp.roundToPx() }
val maxWidth = with(LocalDensity.current) { windowWidth() * density }
Box(Modifier
.fillMaxSize()
.offset { IntOffset(if (newChat.isGone()) -maxWidth.value.roundToInt() else xOffset, 0) }
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { userPickerState.value = AnimatedViewState.HIDING })
.padding(bottom = 10.dp, top = 10.dp)
.graphicsLayer {
alpha = animatedFloat.value
translationY = (if (appPrefs.oneHandUI.state.value) -1 else 1) * (animatedFloat.value - 1) * xOffset
},
contentAlignment = contentAlignment
) {
Column(
Modifier Modifier
.widthIn(min = 260.dp) .fillMaxSize()
.width(IntrinsicSize.Min) .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { userPickerState.value = AnimatedViewState.HIDING }),
.height(IntrinsicSize.Min) contentAlignment = if (appPlatform.isAndroid) Alignment.BottomStart else Alignment.TopStart
.shadow(8.dp, RoundedCornerShape(corner = CornerSize(25.dp)), clip = true)
.background(MaterialTheme.colors.surface, RoundedCornerShape(corner = CornerSize(25.dp)))
.clip(RoundedCornerShape(corner = CornerSize(25.dp)))
) { ) {
val currentRemoteHost = remember { chatModel.currentRemoteHost }.value val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) { if (it == DismissValue.DismissedToEnd) {
if (remoteHosts.isNotEmpty()) { userPickerState.value = AnimatedViewState.HIDING
if (currentRemoteHost == null && chatModel.localUserCreated.value == true) {
LocalDevicePickerItem(true) {
userPickerState.value = AnimatedViewState.HIDING
switchToLocalDevice()
}
Divider(Modifier.requiredHeight(1.dp))
} else if (currentRemoteHost != null) {
val connecting = rememberSaveable { mutableStateOf(false) }
RemoteHostPickerItem(currentRemoteHost,
actionButtonClick = {
userPickerState.value = AnimatedViewState.HIDING
stopRemoteHostAndReloadHosts(currentRemoteHost, true)
}) {
userPickerState.value = AnimatedViewState.HIDING
switchToRemoteHost(currentRemoteHost, connecting)
}
Divider(Modifier.requiredHeight(1.dp))
}
}
UsersView()
if (remoteHosts.isNotEmpty() && currentRemoteHost != null && chatModel.localUserCreated.value == true) {
LocalDevicePickerItem(false) {
userPickerState.value = AnimatedViewState.HIDING
switchToLocalDevice()
}
Divider(Modifier.requiredHeight(1.dp))
}
remoteHosts.filter { !it.activeHost }.forEach { h ->
val connecting = rememberSaveable { mutableStateOf(false) }
RemoteHostPickerItem(h,
actionButtonClick = {
userPickerState.value = AnimatedViewState.HIDING
stopRemoteHostAndReloadHosts(h, false)
}) {
userPickerState.value = AnimatedViewState.HIDING
switchToRemoteHost(h, connecting)
}
Divider(Modifier.requiredHeight(1.dp))
} }
true
} }
if (appPlatform.isAndroid) {
UseFromDesktopPickerItem { val swipeableModifier = DraggableBottomDrawerModifier(
ModalManager.start.showCustomModal { close -> state = dismissState,
ConnectDesktopView(close) swipeDistance = with(LocalDensity.current) { 250.dp.toPx() },
)
Column(
Modifier
.height(IntrinsicSize.Min)
.then(if (appPlatform.isDesktop) Modifier.width(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) else swipeableModifier)
.shadow(8.dp, clip = true)
.fillMaxWidth()
.background(MaterialTheme.colors.surface)
) {
val currentRemoteHost = remember { chatModel.currentRemoteHost }.value
Column(
Modifier
.padding(vertical = DEFAULT_PADDING)
) {
if (remoteHosts.isNotEmpty()) {
val localDeviceActive = currentRemoteHost == null && chatModel.localUserCreated.value == true
DevicePickerRow(
localDeviceActive = localDeviceActive,
remoteHosts = remoteHosts,
onRemoteHostClick = { h, connecting ->
userPickerState.value = AnimatedViewState.HIDING
switchToRemoteHost(h, connecting)
},
onLocalDeviceClick = {
userPickerState.value = AnimatedViewState.HIDING
switchToLocalDevice()
},
onRemoteHostActionButtonClick = { h ->
userPickerState.value = AnimatedViewState.HIDING
stopRemoteHostAndReloadHosts(h, true)
}
)
} }
userPickerState.value = AnimatedViewState.GONE val showCustomModal: (@Composable() (ModalData.(ChatModel, () -> Unit) -> Unit)) -> () -> Unit = { modalView ->
} {
Divider(Modifier.requiredHeight(1.dp)) if (appPlatform.isDesktop) {
} else { userPickerState.value = AnimatedViewState.HIDING
if (remoteHosts.isEmpty()) { }
LinkAMobilePickerItem { ModalManager.start.showCustomModal { close -> modalView(chatModel, close) }
ModalManager.start.showModal {
ConnectMobileView()
} }
userPickerState.value = AnimatedViewState.GONE
} }
Divider(Modifier.requiredHeight(1.dp))
} ActiveUserSection(
if (chatModel.desktopNoUserNoRemote) { chatModel = chatModel,
CreateInitialProfile { userPickerState = userPickerState,
doWithAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { showCustomModal = showCustomModal,
ModalManager.center.showModalCloseable { close -> )
LaunchedEffect(Unit) {
Divider(Modifier.padding(DEFAULT_PADDING))
val profileHidden = rememberSaveable { mutableStateOf(false) }
GlobalSettingsSection(
chatModel = chatModel,
userPickerState = userPickerState,
setPerformLA = setPerformLA,
onUserClicked = { user ->
userPickerState.value = AnimatedViewState.HIDING
if (!user.activeUser) {
withBGApi {
controller.showProgressIfNeeded {
ModalManager.closeAllModalsEverywhere()
chatModel.controller.changeActiveUser(user.remoteHostId, user.userId, null)
}
}
}
},
onShowAllProfilesClicked = {
doWithAuth(
generalGetString(MR.strings.auth_open_chat_profiles),
generalGetString(MR.strings.auth_log_in_using_credential)
) {
if (appPlatform.isDesktop) {
userPickerState.value = AnimatedViewState.HIDING userPickerState.value = AnimatedViewState.HIDING
} }
CreateProfile(chat.simplex.common.platform.chatModel, close) ModalManager.start.showCustomModal { close ->
val search = rememberSaveable { mutableStateOf("") }
ModalView(
{ close() },
endButtons = {
SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
},
content = { UserProfilesView(chatModel, search, profileHidden) })
}
} }
} }
} )
Divider(Modifier.requiredHeight(1.dp))
} }
} }
if (showSettings) {
SettingsPickerItem(settingsClicked)
}
if (showCancel) {
CancelPickerItem(cancelClicked)
}
} }
} }
} }
@@ -296,7 +559,7 @@ fun UserProfilePickerItem(
} }
} else if (!u.showNtfs) { } else if (!u.showNtfs) {
Icon(painterResource(MR.images.ic_notifications_off), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) Icon(painterResource(MR.images.ic_notifications_off), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary)
} else { } else {
Box(Modifier.size(20.dp)) Box(Modifier.size(20.dp))
} }
} }
@@ -325,136 +588,128 @@ fun UserProfileRow(u: User, enabled: Boolean = chatModel.chatRunning.value == tr
} }
@Composable @Composable
fun RemoteHostPickerItem(h: RemoteHostInfo, onLongClick: () -> Unit = {}, actionButtonClick: () -> Unit = {}, onClick: () -> Unit) { private fun DevicePickerRow(
localDeviceActive: Boolean,
remoteHosts: List<RemoteHostInfo>,
onLocalDeviceClick: () -> Unit,
onRemoteHostClick: (rh: RemoteHostInfo, connecting: MutableState<Boolean>) -> Unit,
onRemoteHostActionButtonClick: (rh: RemoteHostInfo) -> Unit,
) {
Row( Row(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.background(color = if (h.activeHost) MaterialTheme.colors.surface.mixWith(MaterialTheme.colors.onBackground, 0.95f) else Color.Unspecified)
.sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT) .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT)
.combinedClickable( .padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING, top = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL),
onClick = onClick, horizontalArrangement = Arrangement.spacedBy(12.dp),
onLongClick = onLongClick
)
.onRightClick { onLongClick() }
.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
RemoteHostRow(h) val activeHost = remoteHosts.firstOrNull { h -> h.activeHost }
if (h.sessionState is RemoteHostSessionState.Connected) {
HostDisconnectButton(actionButtonClick) if (activeHost != null) {
} else { val connecting = rememberSaveable { mutableStateOf(false) }
Box(Modifier.size(20.dp))
DevicePill(
active = true,
icon = painterResource(MR.images.ic_smartphone_300),
text = activeHost.hostDeviceName,
actionButtonVisible = activeHost.sessionState is RemoteHostSessionState.Connected,
onActionButtonClick = { onRemoteHostActionButtonClick(activeHost) }
) {
onRemoteHostClick(activeHost, connecting)
}
}
DevicePill(
active = localDeviceActive,
icon = painterResource(MR.images.ic_desktop),
text = stringResource(MR.strings.this_device),
actionButtonVisible = false
) {
onLocalDeviceClick()
}
remoteHosts.filter { h -> h.sessionState is RemoteHostSessionState.Connected && !h.activeHost }.forEach { h ->
val connecting = rememberSaveable { mutableStateOf(false) }
DevicePill(
active = h.activeHost,
icon = painterResource(MR.images.ic_smartphone_300),
text = h.hostDeviceName,
actionButtonVisible = h.sessionState is RemoteHostSessionState.Connected,
onActionButtonClick = { onRemoteHostActionButtonClick(h) }
) {
onRemoteHostClick(h, connecting)
}
} }
} }
} }
@Composable @Composable
fun RemoteHostRow(h: RemoteHostInfo) { expect fun UserPickerInactiveUsersSection(
Row( users: List<UserInfo>,
Modifier stopped: Boolean,
.widthIn(max = windowWidth() * 0.7f) onShowAllProfilesClicked: () -> Unit,
.padding(start = 17.dp), onUserClicked: (user: User) -> Unit,
verticalAlignment = Alignment.CenterVertically )
) {
Icon(painterResource(MR.images.ic_smartphone_300), h.hostDeviceName, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
Text(
h.hostDeviceName,
modifier = Modifier.padding(start = 26.dp, end = 8.dp),
color = if (h.activeHost) MaterialTheme.colors.onBackground else MenuTextColor,
fontSize = 14.sp,
)
}
}
@Composable @Composable
fun LocalDevicePickerItem(active: Boolean, onLongClick: () -> Unit = {}, onClick: () -> Unit) { expect fun UserPickerScaffold(
pickerState: MutableStateFlow<AnimatedViewState>,
content: @Composable () -> Unit
)
@Composable
fun DevicePill(
active: Boolean,
icon: Painter,
text: String,
actionButtonVisible: Boolean,
onActionButtonClick: (() -> Unit)? = null,
onClick: () -> Unit) {
Row( Row(
Modifier Modifier
.fillMaxWidth() .clip(RoundedCornerShape(8.dp))
.background(color = if (active) MaterialTheme.colors.surface.mixWith(MaterialTheme.colors.onBackground, 0.95f) else Color.Unspecified) .border(
.sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT) BorderStroke(1.dp, MaterialTheme.colors.secondaryVariant),
.combinedClickable( shape = RoundedCornerShape(8.dp)
)
.background(if (active) MaterialTheme.colors.secondaryVariant else MaterialTheme.colors.surface)
.clickable(
onClick = if (active) {{}} else onClick, onClick = if (active) {{}} else onClick,
onLongClick = onLongClick,
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = if (!active) LocalIndication.current else null indication = if (!active) LocalIndication.current else null
),
verticalAlignment = Alignment.CenterVertically
) {
Row(
Modifier.padding(horizontal = 6.dp, vertical = 4.dp)
) {
Icon(
icon,
text,
Modifier.size(16.dp * fontSizeSqrtMultiplier),
tint = MaterialTheme.colors.onSurface
) )
.onRightClick { onLongClick() } Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON * fontSizeSqrtMultiplier))
.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING), Text(
horizontalArrangement = Arrangement.SpaceBetween, text,
verticalAlignment = Alignment.CenterVertically color = MaterialTheme.colors.onSurface,
) { fontSize = 12.sp,
LocalDeviceRow(active) )
Box(Modifier.size(20.dp)) if (onActionButtonClick != null && actionButtonVisible) {
} val interactionSource = remember { MutableInteractionSource() }
} val hovered = interactionSource.collectIsHoveredAsState().value
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON * fontSizeSqrtMultiplier))
@Composable IconButton(onActionButtonClick, Modifier.requiredSize(16.dp * fontSizeSqrtMultiplier)) {
fun LocalDeviceRow(active: Boolean) { Icon(
Row( painterResource(if (hovered) MR.images.ic_wifi_off else MR.images.ic_wifi),
Modifier null,
.widthIn(max = windowWidth() * 0.7f) Modifier.size(16.dp * fontSizeSqrtMultiplier).hoverable(interactionSource),
.padding(start = 17.dp, end = DEFAULT_PADDING), tint = if (hovered) WarningOrange else MaterialTheme.colors.onBackground
verticalAlignment = Alignment.CenterVertically )
) { }
Icon(painterResource(MR.images.ic_desktop), stringResource(MR.strings.this_device), Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground) }
Text( }
stringResource(MR.strings.this_device),
modifier = Modifier.padding(start = 26.dp, end = 8.dp),
color = if (active) MaterialTheme.colors.onBackground else MenuTextColor,
fontSize = 14.sp,
)
}
}
@Composable
private fun UseFromDesktopPickerItem(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
val text = generalGetString(MR.strings.settings_section_title_use_from_desktop).lowercase().capitalize(Locale.current)
Icon(painterResource(MR.images.ic_desktop), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text(text, color = MenuTextColor)
}
}
@Composable
private fun LinkAMobilePickerItem(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
val text = generalGetString(MR.strings.link_a_mobile)
Icon(painterResource(MR.images.ic_smartphone_300), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text(text, color = MenuTextColor)
}
}
@Composable
private fun CreateInitialProfile(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
val text = generalGetString(MR.strings.create_chat_profile)
Icon(painterResource(MR.images.ic_manage_accounts), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text(text, color = MenuTextColor)
}
}
@Composable
private fun SettingsPickerItem(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
val text = generalGetString(MR.strings.settings_section_title_settings).lowercase().capitalize(Locale.current)
Icon(painterResource(MR.images.ic_settings), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text(text, color = MenuTextColor)
}
}
@Composable
private fun CancelPickerItem(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
val text = generalGetString(MR.strings.cancel_verb)
Icon(painterResource(MR.images.ic_close), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text(text, color = MenuTextColor)
} }
} }
@@ -7,3 +7,5 @@ fun <T> chatListAnimationSpec() = tween<T>(durationMillis = 250, easing = FastOu
fun <T> newChatSheetAnimSpec() = tween<T>(256, 0, LinearEasing) fun <T> newChatSheetAnimSpec() = tween<T>(256, 0, LinearEasing)
fun <T> audioProgressBarAnimationSpec() = tween<T>(durationMillis = 30, easing = LinearEasing) fun <T> audioProgressBarAnimationSpec() = tween<T>(durationMillis = 30, easing = LinearEasing)
fun <T> userPickerAnimSpec() = tween<T>(256, 0, FastOutSlowInEasing)
@@ -16,6 +16,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import chat.simplex.common.platform.appPlatform import chat.simplex.common.platform.appPlatform
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chatlist.DevicePill
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 kotlin.math.absoluteValue import kotlin.math.absoluteValue
@@ -157,9 +158,13 @@ private fun bottomTitleAlpha(connection: CollapsingAppBarNestedScrollConnection?
@Composable @Composable
private fun HostDeviceTitle(hostDevice: Pair<Long?, String>, extraPadding: Boolean = false) { 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) { Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) {
Icon(painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300), null, Modifier.size(15.dp), tint = MaterialTheme.colors.secondary) DevicePill(
Spacer(Modifier.width(10.dp)) active = true,
Text(hostDevice.second, color = MaterialTheme.colors.secondary) onClick = {},
actionButtonVisible = false,
icon = painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300),
text = hostDevice.second
)
} }
} }
@@ -208,7 +208,7 @@ private fun RetryButton(onClick: () -> Unit) {
} }
@Composable @Composable
private fun ProfilePickerOption( fun ProfilePickerOption(
title: String, title: String,
selected: Boolean, selected: Boolean,
disabled: Boolean, disabled: Boolean,
@@ -267,7 +267,7 @@ private fun ProfilePickerOption(
) )
} }
private fun filteredProfiles(users: List<User>, searchTextOrPassword: String): List<User> { fun filteredProfiles(users: List<User>, searchTextOrPassword: String): List<User> {
val s = searchTextOrPassword.trim() val s = searchTextOrPassword.trim()
val lower = s.lowercase() val lower = s.lowercase()
return users.filter { u -> return users.filter { u ->
@@ -9,6 +9,7 @@ import SectionView
import androidx.compose.foundation.* import androidx.compose.foundation.*
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.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.MaterialTheme.colors import androidx.compose.material.MaterialTheme.colors
@@ -606,6 +607,38 @@ object AppearanceScope {
} }
} }
@Composable
fun ColorModeSwitcher() {
val currentTheme by CurrentColors.collectAsState()
var themeMode = currentTheme.base.mode;
if (appPrefs.currentTheme.get() === DefaultTheme.SYSTEM_THEME_NAME) {
themeMode = if (systemInDarkThemeCurrently) DefaultThemeMode.DARK else DefaultThemeMode.LIGHT
}
Column(
modifier = Modifier
.clip(CircleShape)
.combinedClickable(
onClick = {
ThemeManager.applyTheme(if (themeMode == DefaultThemeMode.LIGHT) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.themeName)
saveThemeToDatabase(null)
},
onLongClick = {
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
showToast(generalGetString(MR.strings.system_mode_toast))
saveThemeToDatabase(null)
}
)
.size(44.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(painterResource(if (themeMode == DefaultThemeMode.LIGHT) MR.images.ic_light_mode else MR.images.ic_bedtime_moon), stringResource(MR.strings.color_mode_light), tint = MaterialTheme.colors.secondary)
}
}
private var updateBackendJob: Job = Job() private var updateBackendJob: Job = Job()
private fun saveThemeToDatabase(themeUserDestination: Pair<Long, ThemeModeOverrides?>?) { private fun saveThemeToDatabase(themeUserDestination: Pair<Long, ThemeModeOverrides?>?) {
val remoteHostId = chatModel.remoteHostId() val remoteHostId = chatModel.remoteHostId()
@@ -31,13 +31,11 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.migration.MigrateFromDeviceView import chat.simplex.common.views.migration.MigrateFromDeviceView
import chat.simplex.common.views.onboarding.SimpleXInfo import chat.simplex.common.views.onboarding.SimpleXInfo
import chat.simplex.common.views.onboarding.WhatsNewView import chat.simplex.common.views.onboarding.WhatsNewView
import chat.simplex.common.views.remote.ConnectDesktopView
import chat.simplex.common.views.remote.ConnectMobileView
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.* import kotlinx.coroutines.*
@Composable @Composable
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerState: DrawerState) { fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: () -> Unit) {
val user = chatModel.currentUser.value val user = chatModel.currentUser.value
val stopped = chatModel.chatRunning.value == false val stopped = chatModel.chatRunning.value == false
SettingsLayout( SettingsLayout(
@@ -71,10 +69,9 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerSt
} }
}, },
withAuth = ::doWithAuth, withAuth = ::doWithAuth,
drawerState = drawerState,
) )
KeyChangeEffect(chatModel.updatingProgress.value != null) { KeyChangeEffect(chatModel.updatingProgress.value != null) {
drawerState.close() close.invoke()
} }
} }
@@ -96,18 +93,11 @@ fun SettingsLayout(
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> 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,
drawerState: DrawerState,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val closeSettings: () -> Unit = { scope.launch { drawerState.close() } }
val view = LocalMultiplatformView() val view = LocalMultiplatformView()
if (drawerState.isOpen) { LaunchedEffect(Unit) {
BackHandler { hideKeyboard(view)
closeSettings()
}
LaunchedEffect(Unit) {
hideKeyboard(view)
}
} }
val theme = CurrentColors.collectAsState() val theme = CurrentColors.collectAsState()
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
@@ -118,46 +108,22 @@ fun SettingsLayout(
) { ) {
AppBarTitle(stringResource(MR.strings.your_settings)) AppBarTitle(stringResource(MR.strings.your_settings))
SectionView(stringResource(MR.strings.settings_section_title_you)) {
val profileHidden = rememberSaveable { mutableStateOf(false) }
if (profile != null) {
SectionItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) {
ProfilePreview(profile, stopped = stopped)
}
SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.your_chat_profiles), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden, drawerState) } } }, disabled = stopped)
SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped)
ChatPreferencesItem(showCustomModal, stopped = stopped)
} else if (chatModel.localUserCreated.value == false) {
SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.create_chat_profile), {
withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) {
ModalManager.center.showModalCloseable { close ->
LaunchedEffect(Unit) {
closeSettings()
}
CreateProfile(chatModel, close)
}
}
}, disabled = stopped)
}
if (appPlatform.isDesktop) {
SettingsActionItem(painterResource(MR.images.ic_smartphone), stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), showModal { ConnectMobileView() }, disabled = stopped)
} else {
SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal { it, close -> ConnectDesktopView(close) }, disabled = stopped)
}
SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_from_device_to_another_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateFromDeviceView(close) } } }, disabled = stopped)
}
SectionDividerSpaced()
SectionView(stringResource(MR.strings.settings_section_title_settings)) { SectionView(stringResource(MR.strings.settings_section_title_settings)) {
SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped) SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped)
SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView() }, disabled = stopped) SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView() }, disabled = stopped)
SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped) SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped)
SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped) SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped)
SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it) }) SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it) })
DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
} }
SectionDividerSpaced() SectionDividerSpaced()
SectionView(stringResource(MR.strings.settings_section_title_chat_database)) {
DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_from_device_to_another_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateFromDeviceView(close) } } }, disabled = stopped)
}
SectionDividerSpaced()
SectionView(stringResource(MR.strings.settings_section_title_help)) { SectionView(stringResource(MR.strings.settings_section_title_help)) {
SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName ?: "") }, disabled = stopped) SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName ?: "") }, disabled = stopped)
SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped) SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped)
@@ -535,7 +501,6 @@ fun PreviewSettingsLayout() {
showCustomModal = { {} }, showCustomModal = { {} },
showVersion = {}, showVersion = {},
withAuth = { _, _, _ -> }, withAuth = { _, _, _ -> },
drawerState = DrawerState(DrawerValue.Closed),
) )
} }
} }
@@ -174,7 +174,7 @@ private fun UserAddressLayout(
saveAas: (AutoAcceptState, MutableState<AutoAcceptState>) -> Unit, saveAas: (AutoAcceptState, MutableState<AutoAcceptState>) -> Unit,
) { ) {
ColumnWithScrollBar { ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(user?.remoteHostId)) AppBarTitle(stringResource(MR.strings.public_address), hostDevice(user?.remoteHostId))
Column( Column(
Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@@ -230,7 +230,7 @@ private fun UserAddressLayout(
private fun CreateAddressButton(onClick: () -> Unit) { private fun CreateAddressButton(onClick: () -> Unit) {
SettingsActionItem( SettingsActionItem(
painterResource(MR.images.ic_qr_code), painterResource(MR.images.ic_qr_code),
stringResource(MR.strings.create_simplex_address), stringResource(MR.strings.create_public_address),
onClick, onClick,
iconColor = MaterialTheme.colors.primary, iconColor = MaterialTheme.colors.primary,
textColor = MaterialTheme.colors.primary, textColor = MaterialTheme.colors.primary,
@@ -34,6 +34,8 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
KeyChangeEffect(u.value?.remoteHostId, u.value?.userId) { KeyChangeEffect(u.value?.remoteHostId, u.value?.userId) {
close() close()
} }
Log.d(TAG, "here: ${user?.userId}")
if (user != null) { if (user != null) {
var profile by remember { mutableStateOf(user.profile.toProfile()) } var profile by remember { mutableStateOf(user.profile.toProfile()) }
UserProfileLayout( UserProfileLayout(
@@ -36,11 +36,10 @@ import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.* import kotlinx.coroutines.*
@Composable @Composable
fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden: MutableState<Boolean>, drawerState: DrawerState) { fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden: MutableState<Boolean>) {
val searchTextOrPassword = rememberSaveable { search } val searchTextOrPassword = rememberSaveable { search }
val users by remember { derivedStateOf { m.users.map { it.user } } } val users by remember { derivedStateOf { m.users.map { it.user } } }
val filteredUsers by remember { derivedStateOf { filteredUsers(m, searchTextOrPassword.value) } } val filteredUsers by remember { derivedStateOf { filteredUsers(m, searchTextOrPassword.value) } }
val scope = rememberCoroutineScope()
UserProfilesLayout( UserProfilesLayout(
users = users, users = users,
filteredUsers = filteredUsers, filteredUsers = filteredUsers,
@@ -51,12 +50,6 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
addUser = { addUser = {
ModalManager.center.showModalCloseable { close -> ModalManager.center.showModalCloseable { close ->
CreateProfile(m, close) CreateProfile(m, close)
if (appPlatform.isDesktop) {
// Hide settings to allow clicks to pass through to CreateProfile view
DisposableEffectOnGone(always = { scope.launch { drawerState.close() } }) {
// Show settings again to allow intercept clicks to close modals after profile creation finishes
scope.launch(NonCancellable) { drawerState.open() } }
}
} }
}, },
activateUser = { user -> activateUser = { user ->
@@ -700,6 +700,10 @@
<string name="is_verified">%s is verified</string> <string name="is_verified">%s is verified</string>
<string name="is_not_verified">%s is not verified</string> <string name="is_not_verified">%s is not verified</string>
<!-- user picker - UserPicker.kt -->
<string name="create_public_contact_address">Create public address</string>
<string name="your_public_contact_address">Your public address</string>
<!-- settings - SettingsView.kt --> <!-- settings - SettingsView.kt -->
<string name="your_settings">Your settings</string> <string name="your_settings">Your settings</string>
<string name="your_simplex_contact_address">Your SimpleX address</string> <string name="your_simplex_contact_address">Your SimpleX address</string>
@@ -848,6 +852,8 @@
<string name="shutdown_alert_desc">Notifications will stop working until you re-launch the app</string> <string name="shutdown_alert_desc">Notifications will stop working until you re-launch the app</string>
<!-- Address Items - UserAddressView.kt --> <!-- Address Items - UserAddressView.kt -->
<string name="public_address">Public address</string>
<string name="create_public_address">Create public address</string>
<string name="create_address">Create address</string> <string name="create_address">Create address</string>
<string name="delete_address__question">Delete address?</string> <string name="delete_address__question">Delete address?</string>
<string name="your_contacts_will_remain_connected">Your contacts will remain connected.</string> <string name="your_contacts_will_remain_connected">Your contacts will remain connected.</string>
@@ -1149,6 +1155,7 @@
<!-- Settings sections --> <!-- Settings sections -->
<string name="settings_section_title_you">YOU</string> <string name="settings_section_title_you">YOU</string>
<string name="settings_section_title_settings">SETTINGS</string> <string name="settings_section_title_settings">SETTINGS</string>
<string name="settings_section_title_chat_database">CHAT DATABASE</string>
<string name="settings_section_title_help">HELP</string> <string name="settings_section_title_help">HELP</string>
<string name="settings_section_title_support">SUPPORT SIMPLEX CHAT</string> <string name="settings_section_title_support">SUPPORT SIMPLEX CHAT</string>
<string name="settings_section_title_app">APP</string> <string name="settings_section_title_app">APP</string>
@@ -1713,6 +1720,7 @@
<string name="theme_remove_image">Remove image</string> <string name="theme_remove_image">Remove image</string>
<string name="appearance_font_size">Font size</string> <string name="appearance_font_size">Font size</string>
<string name="appearance_zoom">Zoom</string> <string name="appearance_zoom">Zoom</string>
<string name="system_mode_toast">System mode</string>
<!-- Wallpapers --> <!-- Wallpapers -->
<string name="wallpaper_preview_hello_alice">Good afternoon!</string> <string name="wallpaper_preview_hello_alice">Good afternoon!</string>
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M497-481q28-30.5 43.25-70.5T555.5-634q0-42.5-15-82.5T497-787q57.5 8 95.5 51.75t38 101.25q0 57.5-38 101.25T497-481Zm199 308q10-17 15.25-36t5.25-39.09v-38.27q0-34.45-15-65.54-15-31.1-40-55.1 48.23 17.4 89.36 44.95Q792-334.5 792-286.5v38.5q0 30.94-22.03 52.97Q747.94-173 717-173h-21Zm96-348.5h-41.5q-15.5 0-26.5-11T713-559q0-15.5 11-26.5t26.5-11H792V-638q0-15.5 11-26.5t26.5-11q15.5 0 26.5 11t11 26.5v41.5h41.5q15.5 0 26.5 11t11 26.5q0 15.5-11 26.5t-26.5 11H867v41.5q0 15.5-11 26.5t-26.5 11q-15.5 0-26.5-11T792-480v-41.5ZM325.5-479q-64.5 0-109.75-45.25T170.5-634q0-64.5 45.25-109.75T325.5-789q64.5 0 109.75 45.25T480.5-634q0 64.5-45.25 109.75T325.5-479Zm-311 231v-31.03q0-32.97 16.75-60.22t45.27-41.76Q137.5-411 199.75-426.25 262-441.5 325.5-441.5t125.75 15.25Q513.5-411 574.48-381.01q28.52 14.51 45.27 41.76Q636.5-312 636.5-279.03V-248q0 30.94-22.03 52.97Q592.44-173 561.5-173h-472q-30.94 0-52.97-22.03Q14.5-217.06 14.5-248Zm311-306q33 0 56.5-23.5t23.5-56.5q0-33-23.5-56.5T325.5-714q-33 0-56.5 23.5T245.5-634q0 33 23.5 56.5t56.5 23.5Zm-236 306h472v-31q0-11.19-5.5-20.34-5.5-9.16-15-14.16-53.5-26.5-107.17-39.75-53.68-13.25-108.33-13.25-55 0-108.5 13.25T110-313.5q-9.5 5-15 14.16-5.5 9.15-5.5 20.34v31Zm236-386Zm0 386Z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M483.82-81.5q-83.12 0-156.43-31.82-73.32-31.81-127.85-86.25Q145-254 113.25-327.25 81.5-400.5 81.5-484q0-131.24 77-236.62t202.35-146.46q16.05-4.92 29.85 4.83t11.8 25.75q-6.5 88.5 24 170.4t94 145.35Q583.5-458 665.75-427t170.75 24q15-1.5 24.75 12.25t5.25 29.25q-40 125.5-145.35 202.75Q615.81-81.5 483.82-81.5ZM484-139q100.95 0 183.22-57.5Q749.5-254 800-343q-90.29-7.95-173.55-41.22Q543.2-417.5 479.5-481q-63.96-62.86-96.73-145.68Q350-709.5 342.5-798.5q-89 48.5-146.25 131.03Q139-584.95 139-484q0 144.38 100.31 244.69T484-139Zm-5-342Z"/></svg>

After

Width:  |  Height:  |  Size: 640 B

@@ -92,16 +92,16 @@ actual object AudioPlayer: AudioPlayerInterface {
return position return position
} }
override fun stop() { override suspend fun stop() {
if (currentlyPlaying.value == null) return if (currentlyPlaying.value == null) return
player.stop() player.stop()
stopListener() stopListener()
} }
override fun stop(item: ChatItem) = stop(item.file?.fileName) override suspend fun stop(item: ChatItem) = stop(item.file?.fileName)
// FileName or filePath are ok // FileName or filePath are ok
override fun stop(fileName: String?) { override suspend fun stop(fileName: String?) {
if (fileName != null && currentlyPlaying.value?.fileSource?.filePath?.endsWith(fileName) == true) { if (fileName != null && currentlyPlaying.value?.fileSource?.filePath?.endsWith(fileName) == true) {
stop() stop()
} }
@@ -126,7 +126,7 @@ actual object AudioPlayer: AudioPlayerInterface {
progressJob = null progressJob = null
} }
override fun play( override suspend fun play(
fileSource: CryptoFile, fileSource: CryptoFile,
audioPlaying: MutableState<Boolean>, audioPlaying: MutableState<Boolean>,
progress: MutableState<Int>, progress: MutableState<Int>,
@@ -155,19 +155,19 @@ actual object AudioPlayer: AudioPlayerInterface {
realDuration?.let { duration.value = it } realDuration?.let { duration.value = it }
} }
override fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>) { override suspend fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>) {
pro.value = pause() pro.value = pause()
audioPlaying.value = false audioPlaying.value = false
} }
override fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) { override suspend fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) {
pro.value = ms pro.value = ms
if (currentlyPlaying.value?.fileSource?.filePath == filePath) { if (currentlyPlaying.value?.fileSource?.filePath == filePath) {
player.seekTo(ms) player.seekTo(ms)
} }
} }
override fun duration(unencryptedFilePath: String): Int? { override suspend fun duration(unencryptedFilePath: String): Int? {
var res: Int? = null var res: Int? = null
try { try {
val helperPlayer = AudioPlayerComponent().mediaPlayer() val helperPlayer = AudioPlayerComponent().mediaPlayer()
@@ -225,7 +225,9 @@ actual object SoundPlayer: SoundPlayerInterface {
override fun stop() { override fun stop() {
playing = false playing = false
AudioPlayer.stop() withBGApi {
AudioPlayer.stop()
}
} }
} }
@@ -259,6 +261,8 @@ actual object CallSoundsPlayer: CallSoundsPlayerInterface {
override fun stop() { override fun stop() {
playingJob?.cancel() playingJob?.cancel()
AudioPlayer.stop() withBGApi {
AudioPlayer.stop()
}
} }
} }
@@ -60,7 +60,9 @@ actual class VideoPlayer actual constructor(
if (soundEnabled.value) { if (soundEnabled.value) {
RecorderInterface.stopRecording?.invoke() RecorderInterface.stopRecording?.invoke()
} }
AudioPlayer.stop() withBGApi {
AudioPlayer.stop()
}
VideoPlayerHolder.stopAll() VideoPlayerHolder.stopAll()
if (listener.value == null) { if (listener.value == null) {
runCatching { runCatching {
@@ -0,0 +1,73 @@
package chat.simplex.common.views.chatlist
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.User
import chat.simplex.common.model.UserInfo
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.flow.MutableStateFlow
@Composable
actual fun UserPickerInactiveUsersSection(
users: List<UserInfo>,
stopped: Boolean,
onShowAllProfilesClicked: () -> Unit,
onUserClicked: (user: User) -> Unit,
) {
if (users.isNotEmpty()) {
val userRows = users.chunked(5)
val rowsToDisplay = if (userRows.count() > 2) 2 else userRows.count()
val horizontalPadding = DEFAULT_PADDING_HALF + 8.dp
Column(Modifier
.padding(horizontal = horizontalPadding, vertical = DEFAULT_PADDING_HALF)
.height(55.dp * rowsToDisplay + (if (rowsToDisplay > 1) DEFAULT_PADDING else 0.dp))
) {
ColumnWithScrollBar(
verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING)
) {
val spaceBetween = (((DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) - (horizontalPadding)) - (55.dp * 5f)) / 5f
userRows.forEach { row ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(spaceBetween),
) {
row.forEach { u ->
UserPickerInactiveUserBadge(u, stopped, size = 55) {
onUserClicked(u.user)
}
}
}
}
}
}
}
UserPickerOptionRow(
painterResource(MR.images.ic_manage_accounts),
stringResource(MR.strings.your_chat_profiles),
onShowAllProfilesClicked
)
}
@Composable
actual fun UserPickerScaffold(pickerState: MutableStateFlow<AnimatedViewState>, content: @Composable () -> Unit) {
AnimatedVisibility(
visible = pickerState.value.isVisible(),
enter = fadeIn(),
exit = fadeOut()
) {
content()
}
}