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
import android.animation.ArgbEvaluator
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.app.*
import android.content.Context
@@ -8,6 +10,8 @@ import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.*
import android.view.View
import androidx.compose.animation.core.*
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.graphics.Color
@@ -260,7 +264,6 @@ class SimplexApp: Application(), LifecycleEventObserver {
override fun androidSetNightModeIfSupported() {
if (Build.VERSION.SDK_INT < 31) return
val light = if (CurrentColors.value.name == DefaultTheme.SYSTEM_THEME_NAME) {
null
} else {
@@ -275,6 +278,31 @@ class SimplexApp: Application(), LifecycleEventObserver {
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) {
val window = mainActivity.get()?.window ?: return
@Suppress("DEPRECATION")
+1 -1
View File
@@ -102,7 +102,7 @@ kotlin {
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.8")
implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT")
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-websocket:efb2ebf85a")
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(
val userPickerState: MutableStateFlow<AnimatedViewState>,
val scaffoldState: ScaffoldState
)
@Composable
@@ -145,8 +144,7 @@ fun MainScreen() {
userPickerState.value = AnimatedViewState.VISIBLE
}
}
val scaffoldState = rememberScaffoldState()
val settingsState = remember { SettingsViewState(userPickerState, scaffoldState) }
val settingsState = remember { SettingsViewState(userPickerState) }
SetupClipboardListener()
if (appPlatform.isAndroid) {
AndroidScreen(settingsState)
@@ -369,7 +367,6 @@ fun EndPartOfScreen() {
@Composable
fun DesktopScreen(settingsState: SettingsViewState) {
Box {
// 56.dp is a size of unused space of settings drawer
Box(Modifier.width(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier + 56.dp)) {
StartPartOfScreen(settingsState)
}
@@ -388,25 +385,21 @@ fun DesktopScreen(settingsState: SettingsViewState) {
EndPartOfScreen()
}
}
val (userPickerState, scaffoldState ) = settingsState
val (userPickerState ) = settingsState
val scope = rememberCoroutineScope()
if (scaffoldState.drawerState.isOpen || (ModalManager.start.hasModalsOpen && !ModalManager.center.hasModalsOpen)) {
if (ModalManager.start.hasModalsOpen && !ModalManager.center.hasModalsOpen) {
Box(
Modifier
.fillMaxSize()
.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = {
ModalManager.start.closeModals()
scope.launch { settingsState.scaffoldState.drawerState.close() }
})
)
}
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier))
tryOrShowError("UserPicker", error = {}) {
UserPicker(chatModel, userPickerState) {
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
userPickerState.value = AnimatedViewState.GONE
}
UserPicker(chatModel, userPickerState, setPerformLA = AppLock::setPerformLA)
}
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
// (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
if (remove) { AudioPlayer.stop(it) }
if (remove) { withBGApi { AudioPlayer.stop(it) } }
remove
}
}
@@ -1,7 +1,6 @@
package chat.simplex.common.platform
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.*
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.*
@@ -22,6 +21,7 @@ interface PlatformInterface {
fun androidIsBackgroundCallAllowed(): Boolean = true
fun androidSetNightModeIfSupported() {}
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 androidPictureInPictureAllowed(): Boolean = true
fun androidCallEnded() {}
@@ -28,7 +28,7 @@ data class CurrentlyPlayingState(
interface AudioPlayerInterface {
val currentlyPlaying: MutableState<CurrentlyPlayingState?>
fun play(
suspend fun play(
fileSource: CryptoFile,
audioPlaying: MutableState<Boolean>,
progress: MutableState<Int>,
@@ -36,12 +36,12 @@ interface AudioPlayerInterface {
resetOnEnd: Boolean,
smallView: Boolean,
)
fun stop()
fun stop(item: ChatItem)
fun stop(fileName: String?)
fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>)
fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?)
fun duration(unencryptedFilePath: String): Int?
suspend fun stop()
suspend fun stop(item: ChatItem)
suspend fun stop(fileName: String?)
suspend fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>)
suspend fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?)
suspend fun duration(unencryptedFilePath: String): Int?
}
expect object AudioPlayer: AudioPlayerInterface
@@ -109,7 +109,6 @@ fun TerminalLayout(
}
},
contentColor = LocalContentColor.current,
drawerContentColor = LocalContentColor.current,
modifier = Modifier.navigationBarsWithImePadding()
) { contentPadding ->
Surface(
@@ -178,7 +178,9 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
selectedChatItems = selectedChatItems,
back = {
hideKeyboard(view)
AudioPlayer.stop()
withBGApi {
AudioPlayer.stop()
}
chatModel.chatId.value = null
chatModel.groupMembers.clear()
chatModel.groupMembersIndexes.clear()
@@ -654,7 +656,6 @@ fun ChatLayout(
modifier = Modifier.navigationBarsWithImePadding(),
floatingActionButton = { floatingButton.value() },
contentColor = LocalContentColor.current,
drawerContentColor = LocalContentColor.current,
backgroundColor = Color.Unspecified
) { contentPadding ->
val wallpaperImage = MaterialTheme.wallpaper.type.image
@@ -52,10 +52,12 @@ fun ComposeVoiceView(
) {
IconButton(
onClick = {
if (!audioPlaying.value) {
AudioPlayer.play(CryptoFile.plain(filePath), audioPlaying, progress, duration, resetOnEnd = false, smallView = false)
} else {
AudioPlayer.pause(audioPlaying, progress)
withBGApi {
if (!audioPlaying.value) {
AudioPlayer.play(CryptoFile.plain(filePath), audioPlaying, progress, duration, resetOnEnd = false, smallView = false)
} else {
AudioPlayer.pause(audioPlaying, progress)
}
}
},
enabled = finishedRecording
@@ -87,7 +89,9 @@ fun ComposeVoiceView(
if (cancelEnabled) {
IconButton(
onClick = {
AudioPlayer.stop(filePath)
withBGApi {
AudioPlayer.stop(filePath)
}
cancelVoice()
},
modifier = Modifier.padding(0.dp)
@@ -121,7 +125,7 @@ fun FinishedRecordingSlider(backgroundColor: Color, progress: MutableState<Int>,
0.24f)
Slider(
progress.value.toFloat(),
onValueChange = { AudioPlayer.seekTo(it.toInt(), progress, filePath) },
onValueChange = { withBGApi { AudioPlayer.seekTo(it.toInt(), progress, filePath) } },
Modifier
.fillMaxWidth()
.drawBehind {
@@ -62,8 +62,10 @@ fun CIVoiceView(
val play: () -> Unit = {
val playIfExists = {
if (fileSource.value != null) {
AudioPlayer.play(fileSource.value!!, audioPlaying, progress, duration, resetOnEnd = true, smallView = smallView)
brokenAudio = !audioPlaying.value
withBGApi {
AudioPlayer.play(fileSource.value!!, audioPlaying, progress, duration, resetOnEnd = true, smallView = smallView)
brokenAudio = !audioPlaying.value
}
}
}
if (chatModel.connectedToRemote() && fileSource.value == null) {
@@ -75,7 +77,10 @@ fun CIVoiceView(
} else playIfExists()
}
val pause = {
AudioPlayer.pause(audioPlaying, progress)
withBGApi {
AudioPlayer.pause(audioPlaying, progress)
}
Unit
}
val text = remember(ci.file?.fileId, ci.file?.fileStatus) {
derivedStateOf {
@@ -87,7 +92,9 @@ fun CIVoiceView(
}
}
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) {
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.input.TextFieldValue
import androidx.compose.ui.unit.*
import chat.simplex.common.AppLock
import chat.simplex.common.SettingsViewState
import chat.simplex.common.model.*
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 searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
val scope = rememberCoroutineScope()
val (userPickerState, scaffoldState ) = settingsState
val (userPickerState) = settingsState
Scaffold(
topBar = {
if (!oneHandUI.value) {
Column(Modifier.padding(end = endPadding)) {
ChatListToolbar(
scaffoldState.drawerState,
userPickerState,
stopped,
setPerformLA,
)
Divider()
}
@@ -175,30 +175,14 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
Column(Modifier.padding(end = endPadding)) {
Divider()
ChatListToolbar(
scaffoldState.drawerState,
userPickerState,
stopped,
)
}
}
},
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)
}
setPerformLA,
)
}
}
},
contentColor = LocalContentColor.current,
drawerContentColor = LocalContentColor.current,
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
drawerGesturesEnabled = appPlatform.isAndroid,
floatingActionButton = {
if (!oneHandUI.value && searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
FloatingActionButton(
@@ -252,11 +236,8 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
UserPicker(
chatModel = chatModel,
userPickerState = userPickerState,
contentAlignment = if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart
) {
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
userPickerState.value = AnimatedViewState.GONE
}
setPerformLA = AppLock::setPerformLA
)
}
}
}
@@ -278,7 +259,7 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
}
@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 barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
val updatingProgress = remember { chatModel.updatingProgress }.value
@@ -349,18 +330,18 @@ private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableSt
DefaultTopAppBar(
navigationButton = {
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 {
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
val allRead = users
.filter { u -> !u.user.activeUser && !u.user.hidden }
.all { u -> u.unreadCount == 0 }
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
if (users.size == 1 && chatModel.remoteHosts.isEmpty()) {
scope.launch { drawerState.open() }
} else {
userPickerState.value = AnimatedViewState.VISIBLE
}
}
}
},
@@ -23,19 +23,17 @@ import kotlinx.coroutines.flow.MutableStateFlow
@Composable
fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stopped: Boolean) {
var searchInList by rememberSaveable { mutableStateOf("") }
val (userPickerState, scaffoldState) = settingsState
val (userPickerState) = settingsState
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
val oneHandUI = remember { appPrefs.oneHandUI.state }
Scaffold(
Modifier.padding(end = endPadding),
contentColor = LocalContentColor.current,
drawerContentColor = LocalContentColor.current,
scaffoldState = scaffoldState,
topBar = {
if (!oneHandUI.value) {
Column {
ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() }
ShareListToolbar(chatModel, stopped) { searchInList = it.trim() }
Divider()
}
}
@@ -44,7 +42,7 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
if (oneHandUI.value) {
Column {
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 {
@@ -122,7 +105,7 @@ private fun EmptyList() {
}
@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) }
val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false }
if (showSearch) {
@@ -138,7 +121,24 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
.filter { u -> !u.user.activeUser && !u.user.hidden }
.all { u -> u.unreadCount == 0 }
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 = {
@@ -1,24 +1,36 @@
package chat.simplex.common.views.chatlist
import SectionItemView
import SectionView
import TextIconSpaced
import androidx.compose.animation.*
import androidx.compose.animation.core.*
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.collectIsHoveredAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.*
import androidx.compose.material.*
import androidx.compose.material.DrawerDefaults.ScrimOpacity
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.*
import androidx.compose.ui.draw.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.*
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.LocalLayoutDirection
import androidx.compose.ui.text.TextStyle
import dev.icerock.moko.resources.compose.painterResource
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
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.platform.*
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.usersettings.doWithAuth
import chat.simplex.common.views.usersettings.*
import chat.simplex.common.views.usersettings.AppearanceScope.ColorModeSwitcher
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
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
fun UserPicker(
chatModel: ChatModel,
userPickerState: MutableStateFlow<AnimatedViewState>,
showSettings: Boolean = true,
contentAlignment: Alignment = Alignment.TopStart,
showCancel: Boolean = false,
cancelClicked: () -> Unit = {},
useFromDesktopClicked: () -> Unit = {},
settingsClicked: () -> Unit = {},
setPerformLA: (Boolean) -> Unit,
) {
val scope = rememberCoroutineScope()
var newChat by remember { mutableStateOf(userPickerState.value) }
if (newChat.isVisible()) {
BackHandler {
@@ -68,6 +343,7 @@ fun UserPicker(
}
}
val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) }
LaunchedEffect(Unit) {
launch {
userPickerState.collect {
@@ -79,6 +355,7 @@ fun UserPicker(
}
}
}
LaunchedEffect(Unit) {
snapshotFlow { newChat.isVisible() }
.distinctUntilChanged()
@@ -124,128 +401,114 @@ fun UserPicker(
}
}
}
val UsersView: @Composable ColumnScope.() -> Unit = {
users.forEach { u ->
UserProfilePickerItem(u.user, u.unreadCount, openSettings = settingsClicked) {
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(
UserPickerScaffold(pickerState = userPickerState) {
Box(
Modifier
.widthIn(min = 260.dp)
.width(IntrinsicSize.Min)
.height(IntrinsicSize.Min)
.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)))
.fillMaxSize()
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { userPickerState.value = AnimatedViewState.HIDING }),
contentAlignment = if (appPlatform.isAndroid) Alignment.BottomStart else Alignment.TopStart
) {
val currentRemoteHost = remember { chatModel.currentRemoteHost }.value
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
if (remoteHosts.isNotEmpty()) {
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))
val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
if (it == DismissValue.DismissedToEnd) {
userPickerState.value = AnimatedViewState.HIDING
}
true
}
if (appPlatform.isAndroid) {
UseFromDesktopPickerItem {
ModalManager.start.showCustomModal { close ->
ConnectDesktopView(close)
val swipeableModifier = DraggableBottomDrawerModifier(
state = dismissState,
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
}
Divider(Modifier.requiredHeight(1.dp))
} else {
if (remoteHosts.isEmpty()) {
LinkAMobilePickerItem {
ModalManager.start.showModal {
ConnectMobileView()
val showCustomModal: (@Composable() (ModalData.(ChatModel, () -> Unit) -> Unit)) -> () -> Unit = { modalView ->
{
if (appPlatform.isDesktop) {
userPickerState.value = AnimatedViewState.HIDING
}
ModalManager.start.showCustomModal { close -> modalView(chatModel, close) }
}
userPickerState.value = AnimatedViewState.GONE
}
Divider(Modifier.requiredHeight(1.dp))
}
if (chatModel.desktopNoUserNoRemote) {
CreateInitialProfile {
doWithAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) {
ModalManager.center.showModalCloseable { close ->
LaunchedEffect(Unit) {
ActiveUserSection(
chatModel = chatModel,
userPickerState = userPickerState,
showCustomModal = showCustomModal,
)
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
}
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) {
Icon(painterResource(MR.images.ic_notifications_off), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary)
} else {
} else {
Box(Modifier.size(20.dp))
}
}
@@ -325,136 +588,128 @@ fun UserProfileRow(u: User, enabled: Boolean = chatModel.chatRunning.value == tr
}
@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(
Modifier
.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)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick
)
.onRightClick { onLongClick() }
.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING),
horizontalArrangement = Arrangement.SpaceBetween,
.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING, top = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
RemoteHostRow(h)
if (h.sessionState is RemoteHostSessionState.Connected) {
HostDisconnectButton(actionButtonClick)
} else {
Box(Modifier.size(20.dp))
val activeHost = remoteHosts.firstOrNull { h -> h.activeHost }
if (activeHost != null) {
val connecting = rememberSaveable { mutableStateOf(false) }
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
fun RemoteHostRow(h: RemoteHostInfo) {
Row(
Modifier
.widthIn(max = windowWidth() * 0.7f)
.padding(start = 17.dp),
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,
)
}
}
expect fun UserPickerInactiveUsersSection(
users: List<UserInfo>,
stopped: Boolean,
onShowAllProfilesClicked: () -> Unit,
onUserClicked: (user: User) -> Unit,
)
@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(
Modifier
.fillMaxWidth()
.background(color = if (active) MaterialTheme.colors.surface.mixWith(MaterialTheme.colors.onBackground, 0.95f) else Color.Unspecified)
.sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT)
.combinedClickable(
.clip(RoundedCornerShape(8.dp))
.border(
BorderStroke(1.dp, MaterialTheme.colors.secondaryVariant),
shape = RoundedCornerShape(8.dp)
)
.background(if (active) MaterialTheme.colors.secondaryVariant else MaterialTheme.colors.surface)
.clickable(
onClick = if (active) {{}} else onClick,
onLongClick = onLongClick,
interactionSource = remember { MutableInteractionSource() },
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() }
.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
LocalDeviceRow(active)
Box(Modifier.size(20.dp))
}
}
@Composable
fun LocalDeviceRow(active: Boolean) {
Row(
Modifier
.widthIn(max = windowWidth() * 0.7f)
.padding(start = 17.dp, end = DEFAULT_PADDING),
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)
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON * fontSizeSqrtMultiplier))
Text(
text,
color = MaterialTheme.colors.onSurface,
fontSize = 12.sp,
)
if (onActionButtonClick != null && actionButtonVisible) {
val interactionSource = remember { MutableInteractionSource() }
val hovered = interactionSource.collectIsHoveredAsState().value
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON * fontSizeSqrtMultiplier))
IconButton(onActionButtonClick, Modifier.requiredSize(16.dp * fontSizeSqrtMultiplier)) {
Icon(
painterResource(if (hovered) MR.images.ic_wifi_off else MR.images.ic_wifi),
null,
Modifier.size(16.dp * fontSizeSqrtMultiplier).hoverable(interactionSource),
tint = if (hovered) WarningOrange else MaterialTheme.colors.onBackground
)
}
}
}
}
}
@@ -7,3 +7,5 @@ fun <T> chatListAnimationSpec() = tween<T>(durationMillis = 250, easing = FastOu
fun <T> newChatSheetAnimSpec() = tween<T>(256, 0, 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 chat.simplex.common.platform.appPlatform
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chatlist.DevicePill
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import kotlin.math.absoluteValue
@@ -157,9 +158,13 @@ private fun bottomTitleAlpha(connection: CollapsingAppBarNestedScrollConnection?
@Composable
private fun HostDeviceTitle(hostDevice: Pair<Long?, String>, extraPadding: Boolean = false) {
Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) {
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)
Spacer(Modifier.width(10.dp))
Text(hostDevice.second, color = MaterialTheme.colors.secondary)
DevicePill(
active = true,
onClick = {},
actionButtonVisible = false,
icon = painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300),
text = hostDevice.second
)
}
}
@@ -208,7 +208,7 @@ private fun RetryButton(onClick: () -> Unit) {
}
@Composable
private fun ProfilePickerOption(
fun ProfilePickerOption(
title: String,
selected: 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 lower = s.lowercase()
return users.filter { u ->
@@ -9,6 +9,7 @@ import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
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 fun saveThemeToDatabase(themeUserDestination: Pair<Long, ThemeModeOverrides?>?) {
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.onboarding.SimpleXInfo
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 kotlinx.coroutines.*
@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 stopped = chatModel.chatRunning.value == false
SettingsLayout(
@@ -71,10 +69,9 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerSt
}
},
withAuth = ::doWithAuth,
drawerState = drawerState,
)
KeyChangeEffect(chatModel.updatingProgress.value != null) {
drawerState.close()
close.invoke()
}
}
@@ -96,18 +93,11 @@ fun SettingsLayout(
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showVersion: () -> Unit,
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit,
drawerState: DrawerState,
) {
val scope = rememberCoroutineScope()
val closeSettings: () -> Unit = { scope.launch { drawerState.close() } }
val view = LocalMultiplatformView()
if (drawerState.isOpen) {
BackHandler {
closeSettings()
}
LaunchedEffect(Unit) {
hideKeyboard(view)
}
LaunchedEffect(Unit) {
hideKeyboard(view)
}
val theme = CurrentColors.collectAsState()
val uriHandler = LocalUriHandler.current
@@ -118,46 +108,22 @@ fun SettingsLayout(
) {
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)) {
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_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_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it) })
DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
}
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)) {
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)
@@ -535,7 +501,6 @@ fun PreviewSettingsLayout() {
showCustomModal = { {} },
showVersion = {},
withAuth = { _, _, _ -> },
drawerState = DrawerState(DrawerValue.Closed),
)
}
}
@@ -174,7 +174,7 @@ private fun UserAddressLayout(
saveAas: (AutoAcceptState, MutableState<AutoAcceptState>) -> Unit,
) {
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(user?.remoteHostId))
AppBarTitle(stringResource(MR.strings.public_address), hostDevice(user?.remoteHostId))
Column(
Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF),
horizontalAlignment = Alignment.CenterHorizontally,
@@ -230,7 +230,7 @@ private fun UserAddressLayout(
private fun CreateAddressButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_qr_code),
stringResource(MR.strings.create_simplex_address),
stringResource(MR.strings.create_public_address),
onClick,
iconColor = MaterialTheme.colors.primary,
textColor = MaterialTheme.colors.primary,
@@ -34,6 +34,8 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
KeyChangeEffect(u.value?.remoteHostId, u.value?.userId) {
close()
}
Log.d(TAG, "here: ${user?.userId}")
if (user != null) {
var profile by remember { mutableStateOf(user.profile.toProfile()) }
UserProfileLayout(
@@ -36,11 +36,10 @@ import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
@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 users by remember { derivedStateOf { m.users.map { it.user } } }
val filteredUsers by remember { derivedStateOf { filteredUsers(m, searchTextOrPassword.value) } }
val scope = rememberCoroutineScope()
UserProfilesLayout(
users = users,
filteredUsers = filteredUsers,
@@ -51,12 +50,6 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
addUser = {
ModalManager.center.showModalCloseable { 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 ->
@@ -700,6 +700,10 @@
<string name="is_verified">%s is 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 -->
<string name="your_settings">Your settings</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>
<!-- 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="delete_address__question">Delete address?</string>
<string name="your_contacts_will_remain_connected">Your contacts will remain connected.</string>
@@ -1149,6 +1155,7 @@
<!-- Settings sections -->
<string name="settings_section_title_you">YOU</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_support">SUPPORT SIMPLEX CHAT</string>
<string name="settings_section_title_app">APP</string>
@@ -1713,6 +1720,7 @@
<string name="theme_remove_image">Remove image</string>
<string name="appearance_font_size">Font size</string>
<string name="appearance_zoom">Zoom</string>
<string name="system_mode_toast">System mode</string>
<!-- Wallpapers -->
<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
}
override fun stop() {
override suspend fun stop() {
if (currentlyPlaying.value == null) return
player.stop()
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
override fun stop(fileName: String?) {
override suspend fun stop(fileName: String?) {
if (fileName != null && currentlyPlaying.value?.fileSource?.filePath?.endsWith(fileName) == true) {
stop()
}
@@ -126,7 +126,7 @@ actual object AudioPlayer: AudioPlayerInterface {
progressJob = null
}
override fun play(
override suspend fun play(
fileSource: CryptoFile,
audioPlaying: MutableState<Boolean>,
progress: MutableState<Int>,
@@ -155,19 +155,19 @@ actual object AudioPlayer: AudioPlayerInterface {
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()
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
if (currentlyPlaying.value?.fileSource?.filePath == filePath) {
player.seekTo(ms)
}
}
override fun duration(unencryptedFilePath: String): Int? {
override suspend fun duration(unencryptedFilePath: String): Int? {
var res: Int? = null
try {
val helperPlayer = AudioPlayerComponent().mediaPlayer()
@@ -225,7 +225,9 @@ actual object SoundPlayer: SoundPlayerInterface {
override fun stop() {
playing = false
AudioPlayer.stop()
withBGApi {
AudioPlayer.stop()
}
}
}
@@ -259,6 +261,8 @@ actual object CallSoundsPlayer: CallSoundsPlayerInterface {
override fun stop() {
playingJob?.cancel()
AudioPlayer.stop()
withBGApi {
AudioPlayer.stop()
}
}
}
@@ -60,7 +60,9 @@ actual class VideoPlayer actual constructor(
if (soundEnabled.value) {
RecorderInterface.stopRecording?.invoke()
}
AudioPlayer.stop()
withBGApi {
AudioPlayer.stop()
}
VideoPlayerHolder.stopAll()
if (listener.value == null) {
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()
}
}