mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
125 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5f08ad46a | |||
| f2565ef5bf | |||
| d4f56322f7 | |||
| 1a8cb55e70 | |||
| 696ddd7a0d | |||
| b25de876e1 | |||
| d96e6314fa | |||
| a239dee68f | |||
| 072d547c48 | |||
| 9693d9e85d | |||
| c78bb5aa5d | |||
| ab5a13081b | |||
| 041a2e21d3 | |||
| a02275d28e | |||
| ce8b23d2ab | |||
| b77c357704 | |||
| 103c0d4881 | |||
| ca1be83db7 | |||
| 7330361289 | |||
| 6727503803 | |||
| fd6a16f3de | |||
| a4db119fcf | |||
| c1629ba5d2 | |||
| 0e749a99c9 | |||
| 5806b51d8a | |||
| 27e1c738e5 | |||
| 3f92e0f249 | |||
| fbb776a14a | |||
| 68f53ff382 | |||
| af467bccd3 | |||
| 45b82f752a | |||
| 366d043e72 | |||
| 1384cf2470 | |||
| 2bea94fe0d | |||
| 624e49e412 | |||
| 140bc679ab | |||
| 4b61beb237 | |||
| 0db7ab964b | |||
| b993c24be6 | |||
| 3db5c4d3b1 | |||
| f83768d2ce | |||
| 7f28de3b28 | |||
| 1b4aaa0614 | |||
| 940077a5d3 | |||
| 03bf16b97a | |||
| f99eaf884f | |||
| 1adb9ffe79 | |||
| 3968ed2753 | |||
| 69da9532fa | |||
| 18fa5c75dd | |||
| c3c9adde5c | |||
| ff7d1528c4 | |||
| 5c2b16549f | |||
| 249f887531 | |||
| 3d9e032900 | |||
| 7a92513b34 | |||
| 43bc09f9cc | |||
| 2e27107646 | |||
| a29ec774e8 | |||
| 9009744c28 | |||
| 3a1321e4f7 | |||
| 4a87d85589 | |||
| ae58948ee8 | |||
| f9778546dd | |||
| a9b24d18be | |||
| d119477d5c | |||
| bebae7209b | |||
| 1ddd6a8c18 | |||
| aae79ffa5f | |||
| 2868005d2f | |||
| bd8cc1040d | |||
| b878fd21b3 | |||
| ac84e1da64 | |||
| 996d92b313 | |||
| 3329544649 | |||
| d663cc010a | |||
| a278432f5c | |||
| 894a9873cb | |||
| 56789de01e | |||
| e08b9f69e9 | |||
| 67ca556331 | |||
| ed13bc9f27 | |||
| d098f1c5cc | |||
| 95062075f0 | |||
| 8fe4be3c0c | |||
| 1e37e77157 | |||
| e609de18c5 | |||
| 2f97fa6b48 | |||
| f7ab32bdf8 | |||
| 1ab692a5e8 | |||
| 4f4713e5af | |||
| 421e2b34f6 | |||
| 0365485591 | |||
| 5d88ef08eb | |||
| e74aa5b5ea | |||
| b6fea51f69 | |||
| 50b86f7dff | |||
| bfbe3cf062 | |||
| fd1cf5f1ed | |||
| bd8c91585f | |||
| 9f1f54d9e0 | |||
| 82ce157c65 | |||
| a293103ee9 | |||
| 32bd3c941a | |||
| 4907259d2e | |||
| a40dd2591b | |||
| f8072e057c | |||
| 9a6a03ffc3 | |||
| 9ea6b4d595 | |||
| 019e1e1653 | |||
| eddb2e250b | |||
| 27a80cb66c | |||
| 4f9aabce7d | |||
| 712aade625 | |||
| 46ce478676 | |||
| b78512031e | |||
| 3c6eb3899f | |||
| ea01373c1a | |||
| a1325a7695 | |||
| 48398c2119 | |||
| ff8e55599b | |||
| 0cd1208daa | |||
| 2686544723 | |||
| f7a21f9792 | |||
| 96aa960bee |
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
+187
@@ -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()
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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() {}
|
||||
|
||||
+7
-7
@@ -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
|
||||
|
||||
-1
@@ -109,7 +109,6 @@ fun TerminalLayout(
|
||||
}
|
||||
},
|
||||
contentColor = LocalContentColor.current,
|
||||
drawerContentColor = LocalContentColor.current,
|
||||
modifier = Modifier.navigationBarsWithImePadding()
|
||||
) { contentPadding ->
|
||||
Surface(
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
+10
-6
@@ -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 {
|
||||
|
||||
+11
-4
@@ -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) {
|
||||
|
||||
+13
-32
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+22
-22
@@ -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 = {
|
||||
|
||||
+486
-231
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -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)
|
||||
|
||||
+8
-3
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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 ->
|
||||
|
||||
+33
@@ -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()
|
||||
|
||||
+11
-46
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
@@ -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(
|
||||
|
||||
+1
-8
@@ -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 |
+13
-9
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -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 {
|
||||
|
||||
+73
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user