mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-27 19:26:12 +00:00
Merge remote-tracking branch 'origin/dc/core-initial-landing-for-chat' into dc/android-desktop-infinite-scroll
This commit is contained in:
@@ -11,10 +11,13 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.draw.*
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView
|
||||
import chat.simplex.common.model.*
|
||||
@@ -39,14 +42,39 @@ import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@Composable
|
||||
fun AppScreen() {
|
||||
AppBarHandler.appBarMaxHeightPx = with(LocalDensity.current) { AppBarHeight.roundToPx() }
|
||||
SimpleXTheme {
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||
MainScreen()
|
||||
Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||
// This padding applies to landscape view only taking care of navigation bar and holes in screen in status bar area
|
||||
// (because nav bar and holes located on vertical sides of screen in landscape view)
|
||||
val direction = LocalLayoutDirection.current
|
||||
val safePadding = WindowInsets.safeDrawing.asPaddingValues()
|
||||
val cutout = WindowInsets.displayCutout.asPaddingValues()
|
||||
val cutoutStart = cutout.calculateStartPadding(direction)
|
||||
val cutoutEnd = cutout.calculateEndPadding(direction)
|
||||
val cutoutMax = maxOf(cutoutStart, cutoutEnd)
|
||||
val paddingStartUntouched = safePadding.calculateStartPadding(direction)
|
||||
val paddingStart = paddingStartUntouched - cutoutStart
|
||||
val paddingEndUntouched = safePadding.calculateEndPadding(direction)
|
||||
val paddingEnd = paddingEndUntouched - cutoutEnd
|
||||
// Such a strange layout is needed because the main content should be covered by solid color in order to hide overflow
|
||||
// of some elements that may have negative offset (so, can't use Row {}).
|
||||
// To check: go to developer settings of Android, choose Display cutout -> Punch hole, and rotate the phone to landscape, open any chat
|
||||
Box {
|
||||
val fullscreenGallery = remember { chatModel.fullscreenGalleryVisible }
|
||||
Box(Modifier.padding(start = paddingStart + cutoutMax, end = paddingEnd + cutoutMax).consumeWindowInsets(PaddingValues(start = paddingStartUntouched, end = paddingEndUntouched))) {
|
||||
Box(Modifier.drawBehind {
|
||||
if (fullscreenGallery.value) {
|
||||
drawRect(Color.Black, topLeft = Offset(-(paddingStart + cutoutMax).toPx(), 0f), Size(size.width + (paddingStart + cutoutMax).toPx() + (paddingEnd + cutoutMax).toPx(), size.height))
|
||||
}
|
||||
}) {
|
||||
MainScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,7 +166,9 @@ fun MainScreen() {
|
||||
}
|
||||
SetupClipboardListener()
|
||||
if (appPlatform.isAndroid) {
|
||||
AndroidScreen(userPickerState)
|
||||
AndroidWrapInCallLayout {
|
||||
AndroidScreen(userPickerState)
|
||||
}
|
||||
} else {
|
||||
DesktopScreen(userPickerState)
|
||||
}
|
||||
@@ -170,7 +200,9 @@ fun MainScreen() {
|
||||
}
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
ModalManager.fullscreen.showInView()
|
||||
AndroidWrapInCallLayout {
|
||||
ModalManager.fullscreen.showInView()
|
||||
}
|
||||
SwitchingUsersView()
|
||||
}
|
||||
|
||||
@@ -237,19 +269,39 @@ fun MainScreen() {
|
||||
|
||||
val ANDROID_CALL_TOP_PADDING = 40.dp
|
||||
|
||||
@Composable
|
||||
fun AndroidWrapInCallLayout(content: @Composable () -> Unit) {
|
||||
val call = remember { chatModel.activeCall}.value
|
||||
val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted
|
||||
Box {
|
||||
Box(Modifier.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)) {
|
||||
content()
|
||||
}
|
||||
if (call != null && showCallArea) {
|
||||
ActiveCallInteractiveArea(call)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
|
||||
BoxWithConstraints {
|
||||
val call = remember { chatModel.activeCall} .value
|
||||
val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted
|
||||
val currentChatId = remember { mutableStateOf(chatModel.chatId.value) }
|
||||
val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) }
|
||||
val cutout = WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||
val direction = LocalLayoutDirection.current
|
||||
val hasCutout = cutout.calculateStartPadding(direction) + cutout.calculateEndPadding(direction) > 0.dp
|
||||
Box(
|
||||
Modifier
|
||||
// clipping only for devices with cutout currently visible on sides. It prevents showing chat list with open chat view
|
||||
// In order cases it's not needed to use clip
|
||||
.then(if (hasCutout) Modifier.clip(RectangleShape) else Modifier)
|
||||
.graphicsLayer {
|
||||
translationX = -offset.value.dp.toPx()
|
||||
// minOf thing is needed for devices with holes in screen while the user on ChatView rotates his phone from portrait to landscape
|
||||
// because in this case (at least in emulator) maxWidth changes in two steps: big first, smaller on next frame.
|
||||
// But offset is remembered already, so this is a better way than dropping a value of offset
|
||||
translationX = -minOf(offset.value.dp, maxWidth).toPx()
|
||||
}
|
||||
.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)
|
||||
) {
|
||||
StartPartOfScreen(userPickerState)
|
||||
}
|
||||
@@ -271,51 +323,40 @@ fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
|
||||
snapshotFlow { chatModel.chatId.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
if (it == null) {
|
||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
|
||||
onComposed(null)
|
||||
}
|
||||
if (it == null) onComposed(null)
|
||||
currentChatId.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { ModalManager.center.modalCount.value > 0 }
|
||||
.filter { chatModel.chatId.value == null }
|
||||
.collect { modalBackground ->
|
||||
if (chatModel.newChatSheetVisible.value) {
|
||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, appPrefs.oneHandUI.get())
|
||||
} else if (modalBackground) {
|
||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false)
|
||||
} else {
|
||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier
|
||||
.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }
|
||||
.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)
|
||||
.then(if (hasCutout) Modifier.clip(RectangleShape) else Modifier)
|
||||
.graphicsLayer { translationX = maxWidth.toPx() - minOf(offset.value.dp, maxWidth).toPx() }
|
||||
) Box2@{
|
||||
currentChatId.value?.let {
|
||||
ChatView(currentChatId, onComposed)
|
||||
}
|
||||
}
|
||||
if (call != null && showCallArea) {
|
||||
ActiveCallInteractiveArea(call)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StartPartOfScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
|
||||
if (chatModel.setDeliveryReceipts.value) {
|
||||
SetDeliveryReceiptsView(chatModel)
|
||||
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
|
||||
SetDeliveryReceiptsView(chatModel)
|
||||
}
|
||||
} else {
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
if (chatModel.sharedContent.value == null)
|
||||
ChatListView(chatModel, userPickerState, AppLock::setPerformLA, stopped)
|
||||
else
|
||||
ShareListView(chatModel, stopped)
|
||||
if (chatModel.sharedContent.value == null) {
|
||||
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
|
||||
ChatListView(chatModel, userPickerState, AppLock::setPerformLA, stopped)
|
||||
}
|
||||
} else {
|
||||
// LALAL initial load of view doesn't show blur. Focusing text field shows it
|
||||
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler(keyboardCoversBar = false)) {
|
||||
ShareListView(chatModel, stopped)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,9 @@ object ChatModel {
|
||||
// Needed to check for bottom nav bar and to apply or not navigation bar color on Android
|
||||
val newChatSheetVisible = mutableStateOf(false)
|
||||
|
||||
// Needed to apply black color to left/right cutout area on Android
|
||||
val fullscreenGalleryVisible = mutableStateOf(false)
|
||||
|
||||
// preferences
|
||||
val notificationPreviewMode by lazy {
|
||||
mutableStateOf(
|
||||
|
||||
+8
-1
@@ -118,6 +118,9 @@ class AppPreferences {
|
||||
val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true)
|
||||
val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true)
|
||||
val privacyMediaBlurRadius = mkIntPreference(SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS, 0)
|
||||
// Blur broken on Android 12, see https://github.com/chrisbanes/haze/issues/77. And not available before 12
|
||||
val deviceSupportsBlur = appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 32
|
||||
val appearanceBarsBlurRadius = mkIntPreference(SHARED_PREFS_APPEARANCE_BARS_BLUR_RADIUS, if (deviceSupportsBlur) 50 else 0)
|
||||
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
|
||||
val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false)
|
||||
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
|
||||
@@ -223,6 +226,8 @@ class AppPreferences {
|
||||
val chatItemTail = mkBoolPreference(SHARED_PREFS_CHAT_ITEM_TAIL, true)
|
||||
val fontScale = mkFloatPreference(SHARED_PREFS_FONT_SCALE, 1f)
|
||||
val densityScale = mkFloatPreference(SHARED_PREFS_DENSITY_SCALE, 1f)
|
||||
val inAppBarsDefaultAlpha = if (deviceSupportsBlur) 0.875f else 0.975f
|
||||
val inAppBarsAlpha = mkFloatPreference(SHARED_PREFS_IN_APP_BARS_ALPHA, inAppBarsDefaultAlpha)
|
||||
|
||||
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
|
||||
val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0)
|
||||
@@ -244,7 +249,7 @@ class AppPreferences {
|
||||
val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true)
|
||||
val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false)
|
||||
|
||||
val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, appPlatform.isAndroid)
|
||||
val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, true)
|
||||
|
||||
val hintPreferences: List<Pair<SharedPreference<Boolean>, Boolean>> = listOf(
|
||||
laNoticeShown to false,
|
||||
@@ -362,6 +367,7 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles"
|
||||
private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays"
|
||||
private const val SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS = "PrivacyMediaBlurRadius"
|
||||
private const val SHARED_PREFS_APPEARANCE_BARS_BLUR_RADIUS = "AppearanceBarsBlurRadius"
|
||||
const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup"
|
||||
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
|
||||
private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites"
|
||||
@@ -428,6 +434,7 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_CHAT_ITEM_TAIL = "ChatItemTail"
|
||||
private const val SHARED_PREFS_FONT_SCALE = "FontScale"
|
||||
private const val SHARED_PREFS_DENSITY_SCALE = "DensityScale"
|
||||
private const val SHARED_PREFS_IN_APP_BARS_ALPHA = "InAppBarsAlpha"
|
||||
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
|
||||
private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode"
|
||||
private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime"
|
||||
|
||||
+1
-10
@@ -14,20 +14,11 @@ import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import java.io.File
|
||||
|
||||
expect fun Modifier.navigationBarsWithImePadding(): Modifier
|
||||
|
||||
@Composable
|
||||
expect fun ProvideWindowInsets(
|
||||
consumeWindowInsets: Boolean = true,
|
||||
windowInsetsAnimationsEnabled: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
)
|
||||
|
||||
@Composable
|
||||
expect fun Modifier.desktopOnExternalDrag(
|
||||
enabled: Boolean = true,
|
||||
onFiles: (List<File>) -> Unit = {},
|
||||
onImage: (Painter) -> Unit = {},
|
||||
onImage: (File) -> Unit = {},
|
||||
onText: (String) -> Unit = {}
|
||||
): Modifier
|
||||
|
||||
|
||||
+3
-2
@@ -8,6 +8,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import chat.simplex.common.model.ChatId
|
||||
import chat.simplex.common.model.NotificationsMode
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
interface PlatformInterface {
|
||||
@@ -20,12 +21,12 @@ interface PlatformInterface {
|
||||
fun androidChatInitializedAndStarted() {}
|
||||
fun androidIsBackgroundCallAllowed(): Boolean = true
|
||||
fun androidSetNightModeIfSupported() {}
|
||||
fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {}
|
||||
fun androidSetDrawerStatusAndNavBarColor(isLight: Boolean, drawerShadingColor: Color, toolbarOnTop: Boolean, navBarColor: Color) {}
|
||||
fun androidSetStatusAndNavigationBarAppearance(isLightStatusBar: Boolean, isLightNavBar: Boolean, blackNavBar: Boolean = false, themeBackgroundColor: Color = CurrentColors.value.colors.background) {}
|
||||
fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {}
|
||||
fun androidPictureInPictureAllowed(): Boolean = true
|
||||
fun androidCallEnded() {}
|
||||
fun androidRestartNetworkObserver() {}
|
||||
val androidApiLevel: Int? get() = null
|
||||
@Composable fun androidLockPortraitOrientation() {}
|
||||
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
|
||||
@Composable fun desktopShowAppUpdateNotice() {}
|
||||
|
||||
+34
@@ -8,6 +8,7 @@ import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
@@ -21,11 +22,44 @@ expect fun LazyColumnWithScrollBar(
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
userScrollEnabled: Boolean = true,
|
||||
additionalBarOffset: State<Dp>? = null,
|
||||
// by default, this function will include .fillMaxSize() without you doing anything. If you don't need it, pass `false` here
|
||||
// maxSize (at least maxHeight) is needed for blur on appBars to work correctly
|
||||
fillMaxSize: Boolean = true,
|
||||
content: LazyListScope.() -> Unit
|
||||
)
|
||||
|
||||
@Composable
|
||||
expect fun LazyColumnWithScrollBarNoAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
state: LazyListState? = null,
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
reverseLayout: Boolean = false,
|
||||
verticalArrangement: Arrangement.Vertical =
|
||||
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
userScrollEnabled: Boolean = true,
|
||||
additionalBarOffset: State<Dp>? = null,
|
||||
content: LazyListScope.() -> Unit
|
||||
)
|
||||
|
||||
@Composable
|
||||
expect fun ColumnWithScrollBar(
|
||||
modifier: Modifier = Modifier,
|
||||
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||
state: ScrollState? = null,
|
||||
// set true when you want to show something in the center with respected .fillMaxSize()
|
||||
maxIntrinsicSize: Boolean = false,
|
||||
// by default, this function will include .fillMaxSize() without you doing anything. If you don't need it, pass `false` here
|
||||
// maxSize (at least maxHeight) is needed for blur on appBars to work correctly
|
||||
fillMaxSize: Boolean = true,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
)
|
||||
|
||||
@Composable
|
||||
expect fun ColumnWithScrollBarNoAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||
|
||||
+21
-15
@@ -1,14 +1,14 @@
|
||||
package chat.simplex.common.ui.theme
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.ChatController
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
@@ -587,21 +587,27 @@ data class ThemeModeOverride (
|
||||
}
|
||||
}
|
||||
|
||||
fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, shape: Shape = RectangleShape): Modifier {
|
||||
return if (baseTheme == DefaultTheme.SIMPLEX) {
|
||||
this.background(brush = Brush.linearGradient(
|
||||
listOf(
|
||||
CurrentColors.value.colors.background.darker(0.4f),
|
||||
CurrentColors.value.colors.background.lighter(0.4f)
|
||||
),
|
||||
Offset(0f, Float.POSITIVE_INFINITY),
|
||||
Offset(Float.POSITIVE_INFINITY, 0f)
|
||||
), shape = shape)
|
||||
} else {
|
||||
this.background(color = CurrentColors.value.colors.background, shape = shape)
|
||||
fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, bgLayerSize: MutableState<IntSize>?, bgLayer: GraphicsLayer?/*, shape: Shape = RectangleShape*/): Modifier {
|
||||
return drawBehind {
|
||||
copyBackgroundToAppBar(bgLayerSize, bgLayer) {
|
||||
if (baseTheme == DefaultTheme.SIMPLEX) {
|
||||
drawRect(brush = themedBackgroundBrush())
|
||||
} else {
|
||||
drawRect(CurrentColors.value.colors.background)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun themedBackgroundBrush(): Brush = Brush.linearGradient(
|
||||
listOf(
|
||||
CurrentColors.value.colors.background.darker(0.4f),
|
||||
CurrentColors.value.colors.background.lighter(0.4f)
|
||||
),
|
||||
Offset(0f, Float.POSITIVE_INFINITY),
|
||||
Offset(Float.POSITIVE_INFINITY, 0f)
|
||||
)
|
||||
|
||||
val DEFAULT_PADDING = 20.dp
|
||||
val DEFAULT_SPACE_AFTER_ICON = 4.dp
|
||||
val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2
|
||||
|
||||
+1
-6
@@ -1,7 +1,6 @@
|
||||
package chat.simplex.common.ui.theme
|
||||
|
||||
import androidx.compose.material.Colors
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
@@ -107,7 +106,7 @@ object ThemeManager {
|
||||
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
platform.androidSetNightModeIfSupported()
|
||||
val c = CurrentColors.value.colors
|
||||
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !ChatController.appPrefs.oneHandUI.get(), ChatController.appPrefs.oneHandUI.get())
|
||||
platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight)
|
||||
}
|
||||
|
||||
fun changeDarkTheme(theme: String) {
|
||||
@@ -125,10 +124,6 @@ object ThemeManager {
|
||||
themeIds[nonSystemThemeName] = prevValue.themeId
|
||||
appPrefs.currentThemeIds.set(themeIds)
|
||||
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
if (name == ThemeColor.BACKGROUND) {
|
||||
val c = CurrentColors.value.colors
|
||||
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false)
|
||||
}
|
||||
}
|
||||
|
||||
fun applyThemeColor(name: ThemeColor, color: Color? = null, pref: MutableState<ThemeModeOverride>) {
|
||||
|
||||
+74
-73
@@ -7,40 +7,34 @@ import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import chat.simplex.common.views.chat.item.CONSOLE_COMPOSE_LAYOUT_ID
|
||||
import chat.simplex.common.views.chat.item.AdaptingBottomPaddingLayout
|
||||
import chat.simplex.common.views.chatlist.NavigationBarBackground
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun TerminalView(floating: Boolean = false, close: () -> Unit) {
|
||||
fun TerminalView(floating: Boolean = false) {
|
||||
val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }
|
||||
val close = {
|
||||
close()
|
||||
if (appPlatform.isDesktop) {
|
||||
ModalManager.center.closeModals()
|
||||
}
|
||||
}
|
||||
BackHandler(onBack = {
|
||||
close()
|
||||
})
|
||||
TerminalLayout(
|
||||
composeState,
|
||||
floating,
|
||||
sendCommand = { sendCommand(chatModel, composeState) },
|
||||
close
|
||||
)
|
||||
}
|
||||
|
||||
@@ -69,7 +63,6 @@ fun TerminalLayout(
|
||||
composeState: MutableState<ComposeState>,
|
||||
floating: Boolean,
|
||||
sendCommand: () -> Unit,
|
||||
close: () -> Unit
|
||||
) {
|
||||
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
|
||||
val textStyle = remember { mutableStateOf(smallFont) }
|
||||
@@ -77,65 +70,63 @@ fun TerminalLayout(
|
||||
fun onMessageChange(s: String) {
|
||||
composeState.value = composeState.value.copy(message = s)
|
||||
}
|
||||
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
Scaffold(
|
||||
topBar = { CloseSheetBar(close) },
|
||||
bottomBar = {
|
||||
Column {
|
||||
Divider()
|
||||
Box(Modifier.padding(horizontal = 8.dp)) {
|
||||
SendMsgView(
|
||||
composeState = composeState,
|
||||
showVoiceRecordIcon = false,
|
||||
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||
isDirectChat = false,
|
||||
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
|
||||
sendMsgEnabled = true,
|
||||
sendButtonEnabled = true,
|
||||
nextSendGrpInv = false,
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = false,
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
placeholder = "",
|
||||
sendMessage = { sendCommand() },
|
||||
sendLiveMessage = null,
|
||||
updateLiveMessage = null,
|
||||
editPrevMessage = {},
|
||||
onMessageChange = ::onMessageChange,
|
||||
onFilesPasted = {},
|
||||
textStyle = textStyle
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
contentColor = LocalContentColor.current,
|
||||
modifier = Modifier.navigationBarsWithImePadding()
|
||||
) { contentPadding ->
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.fillMaxWidth(),
|
||||
color = MaterialTheme.colors.background,
|
||||
contentColor = LocalContentColor.current
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
val composeViewHeight = remember { mutableStateOf(0.dp) }
|
||||
AdaptingBottomPaddingLayout(Modifier, CONSOLE_COMPOSE_LAYOUT_ID, composeViewHeight) {
|
||||
TerminalLog(floating, composeViewHeight)
|
||||
Column(
|
||||
Modifier
|
||||
.layoutId(CONSOLE_COMPOSE_LAYOUT_ID)
|
||||
.align(Alignment.BottomCenter)
|
||||
.navigationBarsPadding()
|
||||
.consumeWindowInsets(PaddingValues(bottom = if (oneHandUI.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp))
|
||||
.imePadding()
|
||||
.padding(bottom = if (oneHandUI.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp)
|
||||
.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
TerminalLog(floating)
|
||||
Divider()
|
||||
Box(Modifier.padding(horizontal = 8.dp)) {
|
||||
SendMsgView(
|
||||
composeState = composeState,
|
||||
showVoiceRecordIcon = false,
|
||||
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||
isDirectChat = false,
|
||||
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
|
||||
sendMsgEnabled = true,
|
||||
sendButtonEnabled = true,
|
||||
nextSendGrpInv = false,
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = false,
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
placeholder = "",
|
||||
sendMessage = { sendCommand() },
|
||||
sendLiveMessage = null,
|
||||
updateLiveMessage = null,
|
||||
editPrevMessage = {},
|
||||
onMessageChange = ::onMessageChange,
|
||||
onFilesPasted = {},
|
||||
textStyle = textStyle
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!oneHandUI.value) {
|
||||
NavigationBarBackground(true, oneHandUI.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TerminalLog(floating: Boolean) {
|
||||
fun TerminalLog(floating: Boolean, composeViewHeight: State<Dp>) {
|
||||
val reversedTerminalItems by remember {
|
||||
derivedStateOf { chatModel.terminalItems.value.asReversed() }
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState()
|
||||
LaunchedEffect(Unit) {
|
||||
var autoScrollToBottom = true
|
||||
var autoScrollToBottom = listState.firstVisibleItemIndex <= 1
|
||||
launch {
|
||||
snapshotFlow { listState.layoutInfo.totalItemsCount }
|
||||
.filter { autoScrollToBottom }
|
||||
@@ -150,12 +141,21 @@ fun TerminalLog(floating: Boolean) {
|
||||
launch {
|
||||
snapshotFlow { listState.firstVisibleItemIndex }
|
||||
.collect {
|
||||
autoScrollToBottom = listState.firstVisibleItemIndex == 0
|
||||
autoScrollToBottom = it == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
LazyColumnWithScrollBar(reverseLayout = true, state = listState) {
|
||||
LazyColumnWithScrollBar (
|
||||
reverseLayout = true,
|
||||
contentPadding = PaddingValues(
|
||||
top = topPaddingToContent(),
|
||||
bottom = composeViewHeight.value
|
||||
),
|
||||
state = listState,
|
||||
additionalBarOffset = composeViewHeight
|
||||
) {
|
||||
items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item ->
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val rhId = item.remoteHostId
|
||||
val rhIdStr = if (rhId == null) "" else "$rhId "
|
||||
Text(
|
||||
@@ -172,13 +172,15 @@ fun TerminalLog(floating: Boolean) {
|
||||
ModalManager.start
|
||||
}
|
||||
modalPlace.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) {
|
||||
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
val details = item.details
|
||||
.let {
|
||||
if (it.length < 100_000) it
|
||||
else it.substring(0, 100_000)
|
||||
}
|
||||
Text(details, modifier = Modifier.heightIn(max = 50_000.dp).padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
|
||||
ColumnWithScrollBar {
|
||||
SelectionContainer {
|
||||
val details = item.details
|
||||
.let {
|
||||
if (it.length < 100_000) it
|
||||
else it.substring(0, 100_000)
|
||||
}
|
||||
Text(details, modifier = Modifier.heightIn(max = 50_000.dp).padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
|
||||
}
|
||||
}
|
||||
}
|
||||
}.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
@@ -208,8 +210,7 @@ fun PreviewTerminalLayout() {
|
||||
TerminalLayout(
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) },
|
||||
sendCommand = {},
|
||||
floating = false,
|
||||
close = {}
|
||||
floating = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+37
-59
@@ -40,8 +40,6 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
|
||||
val scrollState = rememberScrollState()
|
||||
val keyboardState by getKeyboardState()
|
||||
var savedKeyboardState by remember { mutableStateOf(keyboardState) }
|
||||
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -50,11 +48,9 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
|
||||
val displayName = rememberSaveable { mutableStateOf("") }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
ColumnWithScrollBar(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||
AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING)
|
||||
AppBarTitle(stringResource(MR.strings.create_profile), withPadding = false, bottomPadding = DEFAULT_PADDING)
|
||||
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(
|
||||
stringResource(MR.strings.display_name),
|
||||
@@ -102,7 +98,6 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -111,59 +106,42 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) {
|
||||
val scrollState = rememberScrollState()
|
||||
val keyboardState by getKeyboardState()
|
||||
var savedKeyboardState by remember { mutableStateOf(keyboardState) }
|
||||
val handler = remember { AppBarHandler() }
|
||||
CompositionLocalProvider(
|
||||
LocalAppBarHandler provides handler
|
||||
) {
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.themedBackground(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
CloseSheetBar(close = {
|
||||
if (chatModel.users.none { !it.user.hidden }) {
|
||||
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
} else {
|
||||
close()
|
||||
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
|
||||
ModalView({
|
||||
if (chatModel.users.none { !it.user.hidden }) {
|
||||
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
}) {
|
||||
ColumnWithScrollBar {
|
||||
val displayName = rememberSaveable { mutableStateOf("") }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
Column(if (appPlatform.isAndroid) Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING) else Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally)) {
|
||||
Box(Modifier.align(Alignment.CenterHorizontally)) {
|
||||
AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING, withPadding = false)
|
||||
}
|
||||
})
|
||||
BackHandler(onBack = {
|
||||
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
})
|
||||
ProfileNameField(displayName, stringResource(MR.strings.display_name), { it.trim() == mkValidName(it) }, focusRequester)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Start, padding = PaddingValues(), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
|
||||
ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Start, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
|
||||
}
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
OnboardingActionButton(
|
||||
if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
|
||||
labelId = MR.strings.create_profile_button,
|
||||
onboarding = null,
|
||||
enabled = canCreateProfile(displayName.value),
|
||||
onclick = { createProfileOnboarding(chat.simplex.common.platform.chatModel, displayName.value, close) }
|
||||
)
|
||||
// Reserve space
|
||||
TextButtonBelowOnboardingButton("", null)
|
||||
}
|
||||
|
||||
ColumnWithScrollBar(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
val displayName = rememberSaveable { mutableStateOf("") }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
Column(if (appPlatform.isAndroid) Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING) else Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally)) {
|
||||
Box(Modifier.align(Alignment.CenterHorizontally)) {
|
||||
AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING, withPadding = false)
|
||||
}
|
||||
ProfileNameField(displayName, stringResource(MR.strings.display_name), { it.trim() == mkValidName(it) }, focusRequester)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Start, padding = PaddingValues(), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
|
||||
ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Start, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
|
||||
}
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
OnboardingActionButton(
|
||||
if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
|
||||
labelId = MR.strings.create_profile_button,
|
||||
onboarding = null,
|
||||
enabled = canCreateProfile(displayName.value),
|
||||
onclick = { createProfileOnboarding(chat.simplex.common.platform.chatModel, displayName.value, close) }
|
||||
)
|
||||
// Reserve space
|
||||
TextButtonBelowOnboardingButton("", null)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(300)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
delay(300)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -255,7 +233,6 @@ fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isVal
|
||||
val modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 50.dp)
|
||||
.navigationBarsWithImePadding()
|
||||
.onFocusChanged { focused = it.isFocused }
|
||||
Column(
|
||||
Modifier
|
||||
@@ -289,6 +266,7 @@ fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isVal
|
||||
enabled = true,
|
||||
isError = false,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ fun IncomingCallAlertLayout(
|
||||
acceptCall: () -> Unit
|
||||
) {
|
||||
val color = if (isInDarkTheme()) MaterialTheme.colors.surface else IncomingCallLight
|
||||
Column(Modifier.fillMaxWidth().background(color).padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) {
|
||||
Column(Modifier.fillMaxWidth().background(color).statusBarsPadding().padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) {
|
||||
IncomingCallInfo(invitation, chatModel)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
|
||||
+1
-4
@@ -529,10 +529,7 @@ fun ChatInfoLayout(
|
||||
KeyChangeEffect(chat.id) {
|
||||
scope.launch { scrollState.scrollTo(0) }
|
||||
}
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
|
||||
+4
-4
@@ -276,7 +276,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
|
||||
@Composable
|
||||
fun HistoryTab() {
|
||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
||||
ColumnWithScrollBar {
|
||||
Details()
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
||||
val versions = ciInfo.itemVersions
|
||||
@@ -300,7 +300,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
|
||||
@Composable
|
||||
fun QuoteTab(qi: CIQuote) {
|
||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
||||
ColumnWithScrollBar {
|
||||
Details()
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
@@ -313,7 +313,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
|
||||
@Composable
|
||||
fun ForwardedFromTab(forwardedFromItem: AChatItem) {
|
||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
||||
ColumnWithScrollBar {
|
||||
Details()
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
||||
SectionView {
|
||||
@@ -375,7 +375,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
|
||||
@Composable
|
||||
fun DeliveryTab(memberDeliveryStatuses: List<MemberDeliveryStatus>) {
|
||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
||||
ColumnWithScrollBar {
|
||||
Details()
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
||||
val mss = membersStatuses(chatModel, memberDeliveryStatuses)
|
||||
|
||||
+220
-225
@@ -12,10 +12,11 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.draw.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -107,6 +108,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
}
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler(chatInfo.id, keyboardCoversBar = false)) {
|
||||
when (chatInfo) {
|
||||
is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> {
|
||||
val perChatTheme = remember(chatInfo, CurrentColors.value.base) { if (chatInfo is ChatInfo.Direct) chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null }
|
||||
@@ -550,28 +552,10 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(),
|
||||
showSearch = showSearch
|
||||
)
|
||||
if (appPlatform.isAndroid) {
|
||||
val backgroundColor = MaterialTheme.colors.background
|
||||
val backgroundColorState = rememberUpdatedState(backgroundColor)
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { ModalManager.center.modalCount.value > 0 }
|
||||
.collect { modalBackground ->
|
||||
if (modalBackground) {
|
||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false)
|
||||
} else {
|
||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, backgroundColorState.value, true, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is ChatInfo.ContactConnection -> {
|
||||
val close = { chatModel.chatId.value = null }
|
||||
val handler = remember { AppBarHandler() }
|
||||
CompositionLocalProvider(
|
||||
LocalAppBarHandler provides handler
|
||||
) {
|
||||
ModalView(close, showClose = appPlatform.isAndroid, content = {
|
||||
ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, false, close)
|
||||
})
|
||||
@@ -580,14 +564,9 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
ModalManager.end.closeModals()
|
||||
chatModel.chatItems.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
is ChatInfo.InvalidJSON -> {
|
||||
val close = { chatModel.chatId.value = null }
|
||||
val handler = remember { AppBarHandler() }
|
||||
CompositionLocalProvider(
|
||||
LocalAppBarHandler provides handler
|
||||
) {
|
||||
ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chatInfo.json) } }, content = {
|
||||
InvalidJSONView(chatInfo.json)
|
||||
})
|
||||
@@ -596,10 +575,10 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
ModalManager.end.closeModals()
|
||||
chatModel.chatItems.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -669,81 +648,67 @@ fun ChatLayout(
|
||||
.desktopOnExternalDrag(
|
||||
enabled = remember(attachmentDisabled.value, chatInfo.value?.userCanSend) { mutableStateOf(!attachmentDisabled.value && chatInfo.value?.userCanSend == true) }.value,
|
||||
onFiles = { paths -> composeState.onFilesAttached(paths.map { it.toURI() }) },
|
||||
onImage = {
|
||||
// TODO: file is not saved anywhere?!
|
||||
val tmpFile = File.createTempFile("image", ".bmp", tmpDir)
|
||||
tmpFile.deleteOnExit()
|
||||
chatModel.filesToDelete.add(tmpFile)
|
||||
val uri = tmpFile.toURI()
|
||||
CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(uri), null) }
|
||||
},
|
||||
onImage = { file -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(file.toURI()), null) } },
|
||||
onText = {
|
||||
// Need to parse HTML in order to correctly display the content
|
||||
//composeState.value = composeState.value.copy(message = composeState.value.message + it)
|
||||
},
|
||||
)
|
||||
) {
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
ModalBottomSheetLayout(
|
||||
scrimColor = Color.Black.copy(alpha = 0.12F),
|
||||
modifier = Modifier.navigationBarsWithImePadding(),
|
||||
sheetElevation = 0.dp,
|
||||
sheetContent = {
|
||||
ChooseAttachmentView(
|
||||
attachmentOption,
|
||||
hide = { scope.launch { attachmentBottomSheetState.hide() } }
|
||||
)
|
||||
},
|
||||
sheetState = attachmentBottomSheetState,
|
||||
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
|
||||
) {
|
||||
val floatingButton: MutableState<@Composable () -> Unit> = remember { mutableStateOf({}) }
|
||||
val setFloatingButton = { button: @Composable () -> Unit ->
|
||||
floatingButton.value = button
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (selectedChatItems.value == null) {
|
||||
val chatInfo = chatInfo.value
|
||||
if (chatInfo != null) {
|
||||
ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
|
||||
}
|
||||
} else {
|
||||
SelectedItemsTopToolbar(selectedChatItems)
|
||||
}
|
||||
},
|
||||
bottomBar = composeView,
|
||||
modifier = Modifier.navigationBarsWithImePadding(),
|
||||
floatingActionButton = { floatingButton.value() },
|
||||
contentColor = LocalContentColor.current,
|
||||
backgroundColor = Color.Unspecified
|
||||
) { contentPadding ->
|
||||
val wallpaperImage = MaterialTheme.wallpaper.type.image
|
||||
val wallpaperType = MaterialTheme.wallpaper.type
|
||||
val backgroundColor = MaterialTheme.wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, MaterialTheme.colors.background)
|
||||
val tintColor = MaterialTheme.wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base)
|
||||
BoxWithConstraints(Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colors.background)
|
||||
.then(if (wallpaperImage != null)
|
||||
Modifier.drawWithCache { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) }
|
||||
else
|
||||
Modifier)
|
||||
.padding(contentPadding)
|
||||
) {
|
||||
val remoteHostId = remember { remoteHostId }.value
|
||||
val chatInfo = remember { chatInfo }.value
|
||||
if (chatInfo != null) {
|
||||
ModalBottomSheetLayout(
|
||||
scrimColor = Color.Black.copy(alpha = 0.12F),
|
||||
sheetElevation = 0.dp,
|
||||
sheetContent = {
|
||||
ChooseAttachmentView(
|
||||
attachmentOption,
|
||||
hide = { scope.launch { attachmentBottomSheetState.hide() } }
|
||||
)
|
||||
},
|
||||
sheetState = attachmentBottomSheetState,
|
||||
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
|
||||
) {
|
||||
val composeViewHeight = remember { mutableStateOf(0.dp) }
|
||||
Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer)) {
|
||||
val remoteHostId = remember { remoteHostId }.value
|
||||
val chatInfo = remember { chatInfo }.value
|
||||
AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) {
|
||||
if (chatInfo != null) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
ChatItemsList(
|
||||
remoteHostId, chatInfo, unreadCount, composeState, searchValue,
|
||||
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
|
||||
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadMessages, deleteMessage, deleteMessages,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
|
||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||
setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy
|
||||
setReaction, showItemDetails, markRead, remember { { onComposed(it) } }, developerTools, showViaProxy
|
||||
)
|
||||
}
|
||||
}
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
Box(
|
||||
Modifier
|
||||
.layoutId(CHAT_COMPOSE_LAYOUT_ID)
|
||||
.align(Alignment.BottomCenter)
|
||||
.imePadding()
|
||||
.navigationBarsPadding()
|
||||
.then(if (oneHandUI.value) Modifier.padding(bottom = AppBarHeight * fontSizeSqrtMultiplier) else Modifier)
|
||||
) {
|
||||
composeView()
|
||||
}
|
||||
}
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
if (oneHandUI.value) {
|
||||
StatusBarBackground()
|
||||
} else {
|
||||
NavigationBarBackground(true, oneHandUI.value, noAlpha = true)
|
||||
}
|
||||
Box(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) {
|
||||
if (selectedChatItems.value == null) {
|
||||
if (chatInfo != null) {
|
||||
ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
|
||||
}
|
||||
} else {
|
||||
SelectedItemsTopToolbar(selectedChatItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -751,7 +716,7 @@ fun ChatLayout(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatInfoToolbar(
|
||||
fun BoxScope.ChatInfoToolbar(
|
||||
chatInfo: ChatInfo,
|
||||
back: () -> Unit,
|
||||
info: () -> Unit,
|
||||
@@ -903,21 +868,33 @@ fun ChatInfoToolbar(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DefaultTopAppBar(
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
DefaultAppBar(
|
||||
navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } },
|
||||
title = { ChatInfoToolbarTitle(chatInfo) },
|
||||
onTitleClick = if (chatInfo is ChatInfo.Local) null else info,
|
||||
showSearch = showSearch.value,
|
||||
onTop = !oneHandUI.value,
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
buttons = barButtons
|
||||
buttons = { barButtons.forEach { it() } }
|
||||
)
|
||||
|
||||
Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier))
|
||||
|
||||
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight * fontSizeSqrtMultiplier)) {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
menuItems.forEach { it() }
|
||||
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) {
|
||||
val density = LocalDensity.current
|
||||
val width = remember { mutableStateOf(250.dp) }
|
||||
val height = remember { mutableStateOf(0.dp) }
|
||||
DefaultDropdownMenu(
|
||||
showMenu,
|
||||
modifier = Modifier.onSizeChanged { with(density) {
|
||||
width.value = it.width.toDp().coerceAtLeast(250.dp)
|
||||
if (oneHandUI.value && (appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 30)) height.value = it.height.toDp()
|
||||
} },
|
||||
offset = DpOffset(-width.value, if (oneHandUI.value) -height.value else AppBarHeight)
|
||||
) {
|
||||
if (oneHandUI.value) {
|
||||
menuItems.asReversed().forEach { it() }
|
||||
} else {
|
||||
menuItems.forEach { it() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -961,11 +938,12 @@ private fun ContactVerifiedShield() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BoxWithConstraintsScope.ChatItemsList(
|
||||
fun BoxScope.ChatItemsList(
|
||||
remoteHostId: Long?,
|
||||
chatInfo: ChatInfo,
|
||||
unreadCount: State<Int>,
|
||||
composeState: MutableState<ComposeState>,
|
||||
composeViewHeight: State<Dp>,
|
||||
searchValue: State<String>,
|
||||
useLinkPreviews: Boolean,
|
||||
linkMode: SimplexLinkMode,
|
||||
@@ -990,7 +968,6 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
|
||||
showItemDetails: (ChatInfo, ChatItem) -> Unit,
|
||||
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
||||
setFloatingButton: (@Composable () -> Unit) -> Unit,
|
||||
onComposed: suspend (chatId: String) -> Unit,
|
||||
developerTools: Boolean,
|
||||
showViaProxy: Boolean,
|
||||
@@ -1013,15 +990,17 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
|
||||
Spacer(Modifier.size(8.dp))
|
||||
val reversedChatItems by remember { derivedStateOf { chatModel.chatItems.asReversed() } }
|
||||
val reversedChatItems = remember { derivedStateOf { chatModel.chatItems.asReversed() } }
|
||||
val revealedItems = rememberSaveable { mutableStateOf(setOf<Long>()) }
|
||||
val sections by remember { derivedStateOf { reversedChatItems.putIntoSections(revealedItems.value) } }
|
||||
val sections by remember { derivedStateOf { reversedChatItems.value.putIntoSections(revealedItems.value) } }
|
||||
val preloadItemsEnabled = remember { mutableStateOf(true) }
|
||||
val boundaries = remember { derivedStateOf { sections.map { it.boundary } } }
|
||||
val scrollPosition: (Int) -> Int = { idx -> min(sections.revealedItemCount() - 1, idx + 1 ) }
|
||||
|
||||
PreloadItems(chatInfo.id, listState, ChatPagination.UNTIL_PRELOAD_COUNT, preloadItemsEnabled, boundaries, loadMessages)
|
||||
val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() }
|
||||
|
||||
val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent().roundToPx() })
|
||||
val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
@@ -1032,11 +1011,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
revealedItems.value = setOf()
|
||||
}
|
||||
preloadItemsEnabled.value = true
|
||||
val firstUnreadItem = reversedChatItems.findLast { it.isRcvNew }
|
||||
val firstUnreadItem = reversedChatItems.value.findLast { it.isRcvNew }
|
||||
if (firstUnreadItem != null) {
|
||||
val firstUnreadItemIndexIdx = sections.chatItemPosition(firstUnreadItem.id)
|
||||
if (firstUnreadItemIndexIdx != null) {
|
||||
listState.scrollToItem(scrollPosition(firstUnreadItemIndexIdx), -maxHeightRounded)
|
||||
listState.scrollToItem(scrollPosition(firstUnreadItemIndexIdx), -maxHeight.value)
|
||||
}
|
||||
|
||||
if (chatModel.chatItemsSectionArea[firstUnreadItem.id] != ChatSectionArea.Bottom) {
|
||||
@@ -1055,13 +1034,13 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
}
|
||||
|
||||
val scrollToItem: (Long) -> Unit = { itemId: Long ->
|
||||
val scrollToItem: State<(Long) -> Unit> = remember { mutableStateOf({ itemId: Long ->
|
||||
val index = sections.chatItemPosition(itemId)
|
||||
preloadItemsEnabled.value = false
|
||||
|
||||
if (index != null) {
|
||||
scope.launch {
|
||||
listState.animateScrollToItem(scrollPosition(index), -maxHeightRounded)
|
||||
listState.animateScrollToItem(scrollPosition(index), -maxHeight.value)
|
||||
preloadItemsEnabled.value = true
|
||||
}
|
||||
} else {
|
||||
@@ -1088,15 +1067,15 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
val idx = sections.chatItemPosition(itemId)
|
||||
scope.launch {
|
||||
if (idx != null) {
|
||||
listState.animateScrollToItem(scrollPosition(idx), -maxHeightRounded)
|
||||
listState.animateScrollToItem(scrollPosition(idx), -maxHeight.value)
|
||||
withContext(Dispatchers.Main) {
|
||||
if (!itemsToDrop.isNullOrEmpty()) {
|
||||
itemsToDrop.forEach {
|
||||
chatModel.chatItemsSectionArea.remove(it.id)
|
||||
}
|
||||
chatModel.chatItems.value.removeIf { chatModel.chatItemsSectionArea[it.id] == null }
|
||||
val newIdx = reversedChatItems.indexOfFirst { it.id == itemId }
|
||||
listState.scrollToItem(scrollPosition(newIdx), -maxHeightRounded)
|
||||
val newIdx = reversedChatItems.value.indexOfFirst { it.id == itemId }
|
||||
listState.scrollToItem(scrollPosition(newIdx), -maxHeight.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1107,6 +1086,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// TODO: Having this block on desktop makes ChatItemsList() to recompose twice on chatModel.chatId update instead of once
|
||||
LaunchedEffect(chatInfo.id) {
|
||||
@@ -1126,21 +1106,23 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
)
|
||||
@Composable
|
||||
fun ChatViewListItem(i: Int, sectionItems: SectionItems, cItem: ChatItem, prevItem: ChatItem?, nextItem: ChatItem?) {
|
||||
CompositionLocalProvider(
|
||||
// Makes horizontal and vertical scrolling to coexist nicely.
|
||||
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
|
||||
LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop()
|
||||
) {
|
||||
val provider = {
|
||||
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
|
||||
scope.launch {
|
||||
listState.scrollToItem(
|
||||
kotlin.math.min(reversedChatItems.lastIndex, indexInReversed + 1),
|
||||
-maxHeightRounded
|
||||
)
|
||||
CompositionLocalProvider(
|
||||
// Makes horizontal and vertical scrolling to coexist nicely.
|
||||
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
|
||||
LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop()
|
||||
) {
|
||||
val itemScope = rememberCoroutineScope()
|
||||
val provider = {
|
||||
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
|
||||
itemScope.launch {
|
||||
listState.scrollToItem(
|
||||
kotlin.math.min(reversedChatItems.value.lastIndex, indexInReversed + 1),
|
||||
-maxHeight.value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val revealed = remember { mutableStateOf(sectionItems.revealed) }
|
||||
|
||||
KeyChangeEffect(revealed.value) {
|
||||
@@ -1160,7 +1142,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
tryOrShowError("${cItem.id}ChatItem", error = {
|
||||
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
|
||||
}) {
|
||||
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
|
||||
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem.value, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1168,7 +1150,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?, itemSeparation: ItemSeparation, previousItemSeparation: ItemSeparation?) {
|
||||
val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
|
||||
if (it == DismissValue.DismissedToStart) {
|
||||
scope.launch {
|
||||
itemScope.launch {
|
||||
if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) {
|
||||
if (composeState.value.editing) {
|
||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||
@@ -1353,7 +1335,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
|
||||
if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) {
|
||||
LaunchedEffect(cItem.id) {
|
||||
scope.launch {
|
||||
itemScope.launch {
|
||||
delay(600)
|
||||
val range = if (sectionItems.mergeCategory != null) {
|
||||
val firstItem = sectionItems.items.first()
|
||||
@@ -1377,7 +1359,16 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
ChatItemView(cItem, null, prevItem, itemSeparation, previousItemSeparation)
|
||||
}
|
||||
}
|
||||
LazyColumnWithScrollBar(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.align(Alignment.BottomCenter),
|
||||
state = listState,
|
||||
reverseLayout = true,
|
||||
contentPadding = PaddingValues(
|
||||
top = topPaddingToContent(),
|
||||
bottom = composeViewHeight.value
|
||||
),
|
||||
additionalBarOffset = composeViewHeight
|
||||
) {
|
||||
for (area in sections) {
|
||||
for ((sIdx, section) in area.items.withIndex()) {
|
||||
if (section.revealed) {
|
||||
@@ -1398,13 +1389,14 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reversedChatItems.isNotEmpty()) {
|
||||
if (reversedChatItems.value.isNotEmpty()) {
|
||||
item {
|
||||
DateSeparator(reversedChatItems.last().meta.itemTs)
|
||||
DateSeparator(reversedChatItems.value.last().meta.itemTs)
|
||||
}
|
||||
}
|
||||
}
|
||||
FloatingButtons(chatModel.chatItems, unreadCount, remoteHostId, chatInfo, searchValue, markRead, setFloatingButton, listState) {
|
||||
|
||||
FloatingButtons(chatModel.chatItems, unreadCount, composeViewHeight, remoteHostId, chatInfo, searchValue, markRead, listState) {
|
||||
preloadItemsEnabled.value = false
|
||||
scope.launch {
|
||||
listState.animateScrollToItem(0)
|
||||
@@ -1414,7 +1406,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
|
||||
FloatingDate(
|
||||
Modifier.padding(top = 10.dp).align(Alignment.TopCenter),
|
||||
Modifier.padding(top = 10.dp + topPaddingToContent()).align(Alignment.TopCenter),
|
||||
listState,
|
||||
)
|
||||
|
||||
@@ -1469,86 +1461,64 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BoxWithConstraintsScope.FloatingButtons(
|
||||
fun BoxScope.FloatingButtons(
|
||||
chatItems: State<List<ChatItem>>,
|
||||
unreadCount: State<Int>,
|
||||
composeViewHeight: State<Dp>,
|
||||
remoteHostId: Long?,
|
||||
chatInfo: ChatInfo,
|
||||
searchValue: State<String>,
|
||||
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
||||
setFloatingButton: (@Composable () -> Unit) -> Unit,
|
||||
listState: LazyListState,
|
||||
scrollToLatestItem: () -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var firstVisibleIndex by remember { mutableStateOf(listState.firstVisibleItemIndex) }
|
||||
var lastIndexOfVisibleItems by remember { mutableStateOf(listState.layoutInfo.visibleItemsInfo.lastIndex) }
|
||||
var firstItemIsVisible by remember { mutableStateOf(firstVisibleIndex == 0) }
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.firstVisibleItemIndex }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
firstVisibleIndex = it
|
||||
firstItemIsVisible = firstVisibleIndex == 0
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
// When both snapshotFlows located in one LaunchedEffect second block will never be called because coroutine is paused on first block
|
||||
// so separate them into two LaunchedEffects
|
||||
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastIndex }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
lastIndexOfVisibleItems = it
|
||||
}
|
||||
}
|
||||
val bottomUnreadCount by remember {
|
||||
val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportSize.height } }
|
||||
val bottomUnreadCount = remember {
|
||||
derivedStateOf {
|
||||
if (unreadCount.value == 0) return@derivedStateOf 0
|
||||
val items = chatItems.value
|
||||
val from = items.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems
|
||||
val from = items.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex
|
||||
if (items.size <= from || from < 0) return@derivedStateOf 0
|
||||
|
||||
items.subList(from, items.size).count { it.isRcvNew }
|
||||
}
|
||||
}
|
||||
val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt()
|
||||
|
||||
LaunchedEffect(bottomUnreadCount, firstItemIsVisible) {
|
||||
val showButtonWithCounter = bottomUnreadCount > 0 && !firstItemIsVisible && searchValue.value.isEmpty()
|
||||
val showButtonWithArrow = !showButtonWithCounter && !firstItemIsVisible
|
||||
setFloatingButton(
|
||||
bottomEndFloatingButton(
|
||||
bottomUnreadCount,
|
||||
showButtonWithCounter,
|
||||
showButtonWithArrow,
|
||||
onClickArrowDown = scrollToLatestItem,
|
||||
onClickCounter = {
|
||||
scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount - 1), firstVisibleOffset) }
|
||||
}
|
||||
))
|
||||
}
|
||||
val showBottomButtonWithCounter = remember { derivedStateOf { bottomUnreadCount.value > 0 && listState.firstVisibleItemIndex != 0 && searchValue.value.isEmpty() } }
|
||||
val showBottomButtonWithArrow = remember { derivedStateOf { !showBottomButtonWithCounter.value && listState.firstVisibleItemIndex != 0 } }
|
||||
BottomEndFloatingButton(
|
||||
bottomUnreadCount,
|
||||
showBottomButtonWithCounter,
|
||||
showBottomButtonWithArrow,
|
||||
composeViewHeight,
|
||||
onClickArrowDown = scrollToLatestItem,
|
||||
onClickCounter = {
|
||||
val firstVisibleOffset = (-maxHeight.value * 0.8).toInt()
|
||||
scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount.value - 1), firstVisibleOffset) }
|
||||
}
|
||||
)
|
||||
// Don't show top FAB if is in search
|
||||
if (searchValue.value.isNotEmpty()) return
|
||||
val fabSize = 56.dp
|
||||
val topUnreadCount by remember {
|
||||
derivedStateOf { unreadCount.value - bottomUnreadCount }
|
||||
}
|
||||
val showButtonWithCounter = topUnreadCount > 0
|
||||
val height = with(LocalDensity.current) { maxHeight.toPx() }
|
||||
val topUnreadCount = remember { derivedStateOf { unreadCount.value - bottomUnreadCount.value } }
|
||||
val showDropDown = remember { mutableStateOf(false) }
|
||||
|
||||
TopEndFloatingButton(
|
||||
Modifier.padding(end = DEFAULT_PADDING, top = 24.dp).align(Alignment.TopEnd),
|
||||
Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent()).align(Alignment.TopEnd),
|
||||
topUnreadCount,
|
||||
showButtonWithCounter,
|
||||
onClick = { scope.launch { listState.animateScrollBy(height) } },
|
||||
onClick = { scope.launch { listState.animateScrollBy(maxHeight.value.toFloat()) } },
|
||||
onLongClick = { showDropDown.value = true }
|
||||
)
|
||||
|
||||
Box {
|
||||
DefaultDropdownMenu(showDropDown, offset = DpOffset(this@FloatingButtons.maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) {
|
||||
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) {
|
||||
val density = LocalDensity.current
|
||||
val width = remember { mutableStateOf(250.dp) }
|
||||
DefaultDropdownMenu(
|
||||
showDropDown,
|
||||
modifier = Modifier.onSizeChanged { with(density) { width.value = it.width.toDp().coerceAtLeast(250.dp) } },
|
||||
offset = DpOffset(-DEFAULT_PADDING - width.value, 24.dp + fabSize + topPaddingToContent())
|
||||
) {
|
||||
ItemAction(
|
||||
generalGetString(MR.strings.mark_read),
|
||||
painterResource(MR.images.ic_check),
|
||||
@@ -1556,7 +1526,7 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
||||
val minUnreadItemId = chatModel.chats.value.firstOrNull { it.remoteHostId == remoteHostId && it.id == chatInfo.id }?.chatStats?.minUnreadItemId ?: return@ItemAction
|
||||
markRead(
|
||||
CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.value.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
|
||||
bottomUnreadCount
|
||||
bottomUnreadCount.value
|
||||
)
|
||||
showDropDown.value = false
|
||||
})
|
||||
@@ -1664,12 +1634,11 @@ fun MemberImage(member: GroupMember) {
|
||||
@Composable
|
||||
private fun TopEndFloatingButton(
|
||||
modifier: Modifier = Modifier,
|
||||
unreadCount: Int,
|
||||
showButtonWithCounter: Boolean,
|
||||
unreadCount: State<Int>,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit
|
||||
) = when {
|
||||
showButtonWithCounter -> {
|
||||
unreadCount.value > 0 -> {
|
||||
val interactionSource = interactionSourceWithDetection(onClick, onLongClick)
|
||||
FloatingActionButton(
|
||||
{}, // no action here
|
||||
@@ -1679,7 +1648,7 @@ private fun TopEndFloatingButton(
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
Text(
|
||||
unreadCountStr(unreadCount),
|
||||
unreadCountStr(unreadCount.value),
|
||||
color = MaterialTheme.colors.primary,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
@@ -1689,6 +1658,16 @@ private fun TopEndFloatingButton(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun topPaddingToContent(): Dp {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
return if (oneHandUI.value) {
|
||||
WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
} else {
|
||||
AppBarHeight * fontSizeSqrtMultiplier + WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FloatingDate(
|
||||
modifier: Modifier,
|
||||
@@ -1698,8 +1677,9 @@ private fun FloatingDate(
|
||||
var isNearBottom by remember { mutableStateOf(true) }
|
||||
val lastVisibleItemDate = remember {
|
||||
derivedStateOf {
|
||||
if (listState.layoutInfo.visibleItemsInfo.lastIndex >= 0 && listState.firstVisibleItemIndex >= 0) {
|
||||
val lastVisibleChatItemIndex = chatModel.chatItems.value.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex
|
||||
if (listState.layoutInfo.visibleItemsInfo.lastIndex >= 0) {
|
||||
val lastFullyVisibleOffset = listState.layoutInfo.viewportEndOffset
|
||||
val lastVisibleChatItemIndex = chatModel.chatItems.value.lastIndex - (listState.layoutInfo.visibleItemsInfo.lastOrNull { item -> item.offset + item.size <= lastFullyVisibleOffset && item.size > 0 }?.index ?: 0)
|
||||
val item = chatModel.chatItems.value.getOrNull(lastVisibleChatItemIndex)
|
||||
val timeZone = TimeZone.currentSystemDefault()
|
||||
item?.meta?.itemTs?.toLocalDateTime(timeZone)?.date?.atStartOfDayIn(timeZone)
|
||||
@@ -1886,48 +1866,44 @@ fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (
|
||||
}
|
||||
}
|
||||
|
||||
private fun bottomEndFloatingButton(
|
||||
unreadCount: Int,
|
||||
showButtonWithCounter: Boolean,
|
||||
showButtonWithArrow: Boolean,
|
||||
@Composable
|
||||
private fun BoxScope.BottomEndFloatingButton(
|
||||
unreadCount: State<Int>,
|
||||
showButtonWithCounter: State<Boolean>,
|
||||
showButtonWithArrow: State<Boolean>,
|
||||
composeViewHeight: State<Dp>,
|
||||
onClickArrowDown: () -> Unit,
|
||||
onClickCounter: () -> Unit
|
||||
): @Composable () -> Unit = when {
|
||||
showButtonWithCounter -> {
|
||||
{
|
||||
FloatingActionButton(
|
||||
onClick = onClickCounter,
|
||||
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
|
||||
modifier = Modifier.size(48.dp),
|
||||
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
||||
) {
|
||||
Text(
|
||||
unreadCountStr(unreadCount),
|
||||
color = MaterialTheme.colors.primary,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
) = when {
|
||||
showButtonWithCounter.value -> {
|
||||
FloatingActionButton(
|
||||
onClick = onClickCounter,
|
||||
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
|
||||
modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp),
|
||||
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
||||
) {
|
||||
Text(
|
||||
unreadCountStr(unreadCount.value),
|
||||
color = MaterialTheme.colors.primary,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
showButtonWithArrow -> {
|
||||
{
|
||||
FloatingActionButton(
|
||||
onClick = onClickArrowDown,
|
||||
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
|
||||
modifier = Modifier.size(48.dp),
|
||||
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(MR.images.ic_keyboard_arrow_down),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
showButtonWithArrow.value -> {
|
||||
FloatingActionButton(
|
||||
onClick = onClickArrowDown,
|
||||
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
|
||||
modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp),
|
||||
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(MR.images.ic_keyboard_arrow_down),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
{}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -2054,6 +2030,25 @@ private fun memberNames(member: GroupMember, prevMember: GroupMember?, memCount:
|
||||
}
|
||||
}
|
||||
|
||||
fun Modifier.chatViewBackgroundModifier(
|
||||
colors: Colors,
|
||||
wallpaper: AppWallpaper,
|
||||
backgroundGraphicsLayerSize: MutableState<IntSize>?,
|
||||
backgroundGraphicsLayer: GraphicsLayer?
|
||||
): Modifier {
|
||||
val wallpaperImage = wallpaper.type.image
|
||||
val wallpaperType = wallpaper.type
|
||||
val backgroundColor = wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, colors.background)
|
||||
val tintColor = wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base)
|
||||
|
||||
return this
|
||||
.then(if (wallpaperImage != null)
|
||||
Modifier.drawWithCache { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor, backgroundGraphicsLayerSize, backgroundGraphicsLayer) }
|
||||
else
|
||||
Modifier.drawWithCache { onDrawBehind { copyBackgroundToAppBar(backgroundGraphicsLayerSize, backgroundGraphicsLayer) { drawRect(backgroundColor) } } }
|
||||
)
|
||||
}
|
||||
|
||||
fun chatViewItemsRange(currIndex: Int?, prevHidden: Int?): IntRange? =
|
||||
if (currIndex != null && prevHidden != null && prevHidden > currIndex) {
|
||||
currIndex..prevHidden
|
||||
|
||||
+4
-2
@@ -13,12 +13,14 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.filesToDelete
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
@@ -896,7 +898,7 @@ fun ComposeView(
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(Modifier.background(MaterialTheme.colors.background)) {
|
||||
Box(Modifier.background(MaterialTheme.colors.background)) {
|
||||
Divider()
|
||||
Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) {
|
||||
val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership)
|
||||
@@ -918,7 +920,7 @@ fun ComposeView(
|
||||
&& !nextSendGrpInv.value
|
||||
IconButton(
|
||||
attachmentClicked,
|
||||
Modifier.padding(bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier),
|
||||
Modifier.padding(start = 3.dp, end = 1.dp, bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier),
|
||||
enabled = attachmentEnabled
|
||||
) {
|
||||
Icon(
|
||||
|
||||
+1
-4
@@ -81,10 +81,7 @@ private fun ContactPreferencesLayout(
|
||||
reset: () -> Unit,
|
||||
savePrefs: () -> Unit,
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.contact_preferences))
|
||||
val timedMessages: MutableState<Boolean> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) }
|
||||
val onTTLUpdated = { ttl: Int? ->
|
||||
|
||||
+4
-3
@@ -1,9 +1,11 @@
|
||||
package chat.simplex.common.views.chat
|
||||
|
||||
import SectionBottomSpacer
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.QRCodeScanner
|
||||
@@ -12,9 +14,7 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
|
||||
Column(
|
||||
Modifier.fillMaxSize()
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.scan_code))
|
||||
QRCodeScanner { text ->
|
||||
verifyCode(text) {
|
||||
@@ -28,5 +28,6 @@ fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: ()
|
||||
}
|
||||
}
|
||||
Text(stringResource(MR.strings.scan_code_from_contacts_app), Modifier.padding(horizontal = DEFAULT_PADDING))
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
+8
-4
@@ -12,6 +12,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.BackHandler
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -20,11 +21,12 @@ import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
|
||||
@Composable
|
||||
fun SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>) {
|
||||
fun BoxScope.SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>) {
|
||||
val onBackClicked = { selectedChatItems.value = null }
|
||||
BackHandler(onBack = onBackClicked)
|
||||
val count = selectedChatItems.value?.size ?: 0
|
||||
DefaultTopAppBar(
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
DefaultAppBar(
|
||||
navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) },
|
||||
title = {
|
||||
Text(
|
||||
@@ -39,10 +41,9 @@ fun SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>) {
|
||||
)
|
||||
},
|
||||
onTitleClick = null,
|
||||
showSearch = false,
|
||||
onTop = !oneHandUI.value,
|
||||
onSearchValueChanged = {},
|
||||
)
|
||||
Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier))
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -68,6 +69,8 @@ fun SelectedItemsBottomToolbar(
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
.background(MaterialTheme.colors.background)
|
||||
.padding(horizontal = 2.dp)
|
||||
.height(AppBarHeight * fontSizeSqrtMultiplier)
|
||||
.pointerInput(Unit) {
|
||||
detectGesture {
|
||||
true
|
||||
@@ -103,6 +106,7 @@ fun SelectedItemsBottomToolbar(
|
||||
)
|
||||
}
|
||||
}
|
||||
Divider(Modifier.align(Alignment.TopStart))
|
||||
}
|
||||
LaunchedEffect(chatInfo, chatItems, selectedChatItems.value) {
|
||||
recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited)
|
||||
|
||||
+8
-5
@@ -7,7 +7,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.*
|
||||
@@ -15,6 +14,7 @@ import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.*
|
||||
@@ -61,7 +61,8 @@ fun SendMsgView(
|
||||
) {
|
||||
val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) }
|
||||
|
||||
Box(Modifier.padding(vertical = if (appPlatform.isAndroid) 8.dp else 6.dp)) {
|
||||
val padding = if (appPlatform.isAndroid) PaddingValues(vertical = 8.dp) else PaddingValues(top = 3.dp, bottom = 4.dp)
|
||||
Box(Modifier.padding(padding)) {
|
||||
val cs = composeState.value
|
||||
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
||||
LaunchedEffect(composeState.value.inProgress) {
|
||||
@@ -147,7 +148,7 @@ fun SendMsgView(
|
||||
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
|
||||
&& cs.contextItem is ComposeContextItem.NoContextItem
|
||||
) {
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Spacer(Modifier.width(12.dp))
|
||||
StartLiveMessageButton(userCanSend) {
|
||||
if (composeState.value.preview is ComposePreview.NoPreview) {
|
||||
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
|
||||
@@ -423,6 +424,7 @@ private fun SendMsgButton(
|
||||
onLongClick: (() -> Unit)? = null
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val ripple = remember { ripple(bounded = false, radius = 24.dp) }
|
||||
Box(
|
||||
modifier = Modifier.requiredSize(36.dp)
|
||||
.combinedClickable(
|
||||
@@ -431,7 +433,7 @@ private fun SendMsgButton(
|
||||
enabled = enabled,
|
||||
role = Role.Button,
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(bounded = false, radius = 24.dp)
|
||||
indication = ripple
|
||||
)
|
||||
.onRightClick { onLongClick?.invoke() },
|
||||
contentAlignment = Alignment.Center
|
||||
@@ -454,6 +456,7 @@ private fun SendMsgButton(
|
||||
@Composable
|
||||
private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val ripple = remember { ripple(bounded = false, radius = 24.dp) }
|
||||
Box(
|
||||
modifier = Modifier.requiredSize(36.dp)
|
||||
.clickable(
|
||||
@@ -461,7 +464,7 @@ private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
enabled = enabled,
|
||||
role = Role.Button,
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(bounded = false, radius = 24.dp)
|
||||
indication = ripple
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
||||
+1
-5
@@ -56,11 +56,7 @@ private fun VerifyCodeLayout(
|
||||
connectionVerified: Boolean,
|
||||
verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit,
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||
AppBarTitle(stringResource(MR.strings.security_code), withPadding = false)
|
||||
val splitCode = splitToParts(connectionCode, 24)
|
||||
Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) {
|
||||
|
||||
+1
-4
@@ -130,10 +130,7 @@ fun AddGroupMembersLayout(
|
||||
}
|
||||
}
|
||||
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.button_add_members))
|
||||
profileText()
|
||||
Spacer(Modifier.size(DEFAULT_PADDING))
|
||||
|
||||
+13
-2
@@ -25,6 +25,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -283,9 +284,14 @@ fun ModalData.GroupChatInfoLayout(
|
||||
if (s.isEmpty()) members else members.filter { m -> m.anyNameContains(s) }
|
||||
}
|
||||
}
|
||||
Box {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
contentPadding = if (oneHandUI.value) {
|
||||
PaddingValues(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp, bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding())
|
||||
} else {
|
||||
PaddingValues(top = topPaddingToContent())
|
||||
},
|
||||
state = listState
|
||||
) {
|
||||
item {
|
||||
@@ -397,6 +403,11 @@ fun ModalData.GroupChatInfoLayout(
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
|
||||
}
|
||||
}
|
||||
if (!oneHandUI.value) {
|
||||
NavigationBarBackground(oneHandUI.value, oneHandUI.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-3
@@ -119,9 +119,7 @@ fun GroupLinkLayout(
|
||||
)
|
||||
}
|
||||
|
||||
ColumnWithScrollBar(
|
||||
Modifier,
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.group_link))
|
||||
Text(
|
||||
stringResource(MR.strings.you_can_share_group_link_anybody_will_be_able_to_connect),
|
||||
|
||||
+1
-4
@@ -314,10 +314,7 @@ fun GroupMemberInfoLayout(
|
||||
}
|
||||
}
|
||||
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
|
||||
+1
-3
@@ -82,9 +82,7 @@ private fun GroupPreferencesLayout(
|
||||
reset: () -> Unit,
|
||||
savePrefs: () -> Unit,
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.group_preferences))
|
||||
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) }
|
||||
val onTTLUpdated = { ttl: Int? ->
|
||||
|
||||
+2
-6
@@ -82,10 +82,9 @@ fun GroupProfileLayout(
|
||||
}, close)
|
||||
}
|
||||
}
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
ModalBottomSheetLayout(
|
||||
scrimColor = Color.Black.copy(alpha = 0.12F),
|
||||
modifier = Modifier.navigationBarsWithImePadding(),
|
||||
modifier = Modifier.imePadding(),
|
||||
sheetContent = {
|
||||
GetImageBottomSheet(
|
||||
chosenImage,
|
||||
@@ -98,9 +97,7 @@ fun GroupProfileLayout(
|
||||
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
|
||||
) {
|
||||
ModalView(close = closeWithAlert) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
Column(
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
@@ -177,7 +174,6 @@ fun GroupProfileLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun canUpdateProfile(displayName: String, groupProfile: GroupProfile): Boolean =
|
||||
|
||||
+1
-3
@@ -95,9 +95,7 @@ private fun GroupWelcomeLayout(
|
||||
linkMode: SimplexLinkMode,
|
||||
save: () -> Unit,
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
val editMode = remember { mutableStateOf(true) }
|
||||
AppBarTitle(stringResource(MR.strings.group_welcome_title))
|
||||
val wt = rememberSaveable { welcomeText }
|
||||
|
||||
+67
@@ -10,6 +10,7 @@ import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.*
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -326,6 +327,8 @@ fun CIMarkdownText(
|
||||
|
||||
const val CHAT_IMAGE_LAYOUT_ID = "chatImage"
|
||||
const val CHAT_BUBBLE_LAYOUT_ID = "chatBubble"
|
||||
const val CHAT_COMPOSE_LAYOUT_ID = "chatCompose"
|
||||
const val CONSOLE_COMPOSE_LAYOUT_ID = "consoleCompose"
|
||||
/**
|
||||
* Equal to [androidx.compose.ui.unit.Constraints.MaxFocusMask], which is 0x3FFFF - 1
|
||||
* Other values make a crash `java.lang.IllegalArgumentException: Can't represent a width of 123456 and height of 9909 in Constraints`
|
||||
@@ -404,6 +407,70 @@ fun DependentLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The purpose of this layout is to make measuring of bottom compose view and adapt top lazy column to its size in the same frame (not on the next frame as you would expect).
|
||||
// So, steps are:
|
||||
// - measuring the layout: measured height of compose view before this step is 0, it's added to content padding of lazy column (so it's == 0)
|
||||
// - measured the layout: measured height of compose view now is correct, but it's not yet applied to lazy column content padding (so it's == 0) and lazy column is placed higher than compose view in view with respect to compose view's height
|
||||
// - on next frame measured height is correct and content padding is the same, lazy column placed to occupy all parent view's size
|
||||
// - every added/removed line in compose view goes through the same process.
|
||||
@Composable
|
||||
fun AdaptingBottomPaddingLayout(
|
||||
modifier: Modifier = Modifier,
|
||||
mainLayoutId: String,
|
||||
expectedHeight: MutableState<Dp>,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val expected = with(LocalDensity.current) { expectedHeight.value.roundToPx() }
|
||||
Layout(
|
||||
content = content,
|
||||
modifier = modifier
|
||||
) { measureable, constraints ->
|
||||
require(measureable.size <= 2) { "Should be exactly one or two elements in this layout, you have ${measureable.size}" }
|
||||
val mainPlaceable = measureable.firstOrNull { it.layoutId == mainLayoutId }!!.measure(constraints)
|
||||
val placeables: List<Placeable> = measureable.map {
|
||||
if (it.layoutId == mainLayoutId)
|
||||
mainPlaceable
|
||||
else
|
||||
it.measure(constraints.copy(maxHeight = if (expected != mainPlaceable.measuredHeight) constraints.maxHeight - mainPlaceable.measuredHeight + expected else constraints.maxHeight)) }
|
||||
expectedHeight.value = mainPlaceable.measuredHeight.toDp()
|
||||
layout(constraints.maxWidth, constraints.maxHeight) {
|
||||
var y = 0
|
||||
placeables.forEach {
|
||||
if (it !== mainPlaceable) {
|
||||
it.place(0, y)
|
||||
y += it.measuredHeight
|
||||
} else {
|
||||
it.place(0, constraints.maxHeight - mainPlaceable.measuredHeight)
|
||||
y += it.measuredHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CenteredRowLayout(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Layout(
|
||||
content = content,
|
||||
modifier = modifier
|
||||
) { measureable, constraints ->
|
||||
require(measureable.size == 3) { "Should be exactly three elements in this layout, you have ${measureable.size}" }
|
||||
val first = measureable[0].measure(constraints.copy(minWidth = 0, minHeight = 0))
|
||||
val third = measureable[2].measure(constraints.copy(minWidth = first.measuredWidth, minHeight = 0))
|
||||
val second = measureable[1].measure(constraints.copy(minWidth = 0, minHeight = 0, maxWidth = (constraints.maxWidth - first.measuredWidth - third.measuredWidth).coerceAtLeast(0)))
|
||||
// Limit width for every other element to width of important element and height for a sum of all elements.
|
||||
layout(constraints.maxWidth, constraints.maxHeight) {
|
||||
first.place(0, ((constraints.maxHeight - first.measuredHeight) / 2).coerceAtLeast(0))
|
||||
second.place((constraints.maxWidth - second.measuredWidth) / 2, ((constraints.maxHeight - second.measuredHeight) / 2).coerceAtLeast(0))
|
||||
third.place(constraints.maxWidth - third.measuredWidth, ((constraints.maxHeight - third.measuredHeight) / 2).coerceAtLeast(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
class EditedProvider: PreviewParameterProvider<Boolean> {
|
||||
|
||||
+10
-3
@@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.CryptoFile
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
@@ -58,9 +57,17 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
||||
val playersToRelease = rememberSaveable { mutableSetOf<URI>() }
|
||||
DisposableEffectOnGone(
|
||||
always = {
|
||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, Color.Black, false, false)
|
||||
platform.androidSetStatusAndNavigationBarAppearance(false, false, blackNavBar = true)
|
||||
chatModel.fullscreenGalleryVisible.value = true
|
||||
},
|
||||
whenGone = { playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) } }
|
||||
whenDispose = {
|
||||
val c = CurrentColors.value.colors
|
||||
platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight)
|
||||
chatModel.fullscreenGalleryVisible.value = false
|
||||
},
|
||||
whenGone = {
|
||||
playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) }
|
||||
}
|
||||
)
|
||||
|
||||
@Composable
|
||||
|
||||
+229
-164
@@ -10,8 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
@@ -34,6 +33,7 @@ import chat.simplex.common.views.onboarding.shouldShowWhatsNew
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.call.Call
|
||||
import chat.simplex.common.views.chat.item.CIFileViewScope
|
||||
import chat.simplex.common.views.chat.topPaddingToContent
|
||||
import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.res.MR
|
||||
@@ -41,7 +41,6 @@ import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.net.URI
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private fun showNewChatSheet(oneHandUI: State<Boolean>) {
|
||||
@@ -55,7 +54,7 @@ private fun showNewChatSheet(oneHandUI: State<Boolean>) {
|
||||
chatModel.newChatSheetVisible.value = false
|
||||
close()
|
||||
}
|
||||
ModalView(close, closeOnTop = !oneHandUI.value) {
|
||||
ModalView(close, showAppBar = !oneHandUI.value) {
|
||||
if (appPlatform.isAndroid) {
|
||||
BackHandler {
|
||||
close()
|
||||
@@ -122,11 +121,7 @@ fun ToggleChatListCard() {
|
||||
|
||||
SharedPreferenceToggle(
|
||||
appPrefs.oneHandUI,
|
||||
enabled = true,
|
||||
onChange = {
|
||||
val c = CurrentColors.value.colors
|
||||
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
|
||||
}
|
||||
enabled = true
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -154,74 +149,36 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow<Animate
|
||||
}
|
||||
}
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (!oneHandUI.value) {
|
||||
Column {
|
||||
ChatListToolbar(
|
||||
userPickerState,
|
||||
stopped,
|
||||
setPerformLA,
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
if (oneHandUI.value) {
|
||||
ChatListWithLoadingScreen(searchText, listState)
|
||||
Column(Modifier.align(Alignment.BottomCenter)) {
|
||||
ChatListToolbar(
|
||||
userPickerState,
|
||||
listState,
|
||||
stopped,
|
||||
setPerformLA,
|
||||
)
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
if (oneHandUI.value) {
|
||||
Column {
|
||||
Divider()
|
||||
ChatListToolbar(
|
||||
userPickerState,
|
||||
stopped,
|
||||
setPerformLA,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ChatListWithLoadingScreen(searchText, listState)
|
||||
Column {
|
||||
ChatListToolbar(
|
||||
userPickerState,
|
||||
listState,
|
||||
stopped,
|
||||
setPerformLA,
|
||||
)
|
||||
}
|
||||
},
|
||||
contentColor = LocalContentColor.current,
|
||||
floatingActionButton = {
|
||||
if (!oneHandUI.value && searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (!stopped) {
|
||||
showNewChatSheet(oneHandUI)
|
||||
}
|
||||
},
|
||||
Modifier
|
||||
.padding(end = DEFAULT_PADDING - 16.dp, bottom = DEFAULT_PADDING - 16.dp)
|
||||
.size(AppBarHeight * fontSizeSqrtMultiplier),
|
||||
elevation = FloatingActionButtonDefaults.elevation(
|
||||
defaultElevation = 0.dp,
|
||||
pressedElevation = 0.dp,
|
||||
hoveredElevation = 0.dp,
|
||||
focusedElevation = 0.dp,
|
||||
),
|
||||
backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
contentColor = Color.White
|
||||
) {
|
||||
Icon(painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(22.dp * fontSizeSqrtMultiplier))
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Box(Modifier.padding(it)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
ChatList(chatModel, searchText = searchText)
|
||||
}
|
||||
if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
|
||||
Text(stringResource(
|
||||
if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
if (searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
|
||||
NewChatSheetFloatingButton(oneHandUI, stopped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (searchText.value.text.isEmpty()) {
|
||||
if (appPlatform.isDesktop) {
|
||||
if (appPlatform.isDesktop && !oneHandUI.value) {
|
||||
val call = remember { chatModel.activeCall }.value
|
||||
if (call != null) {
|
||||
ActiveCallInteractiveArea(call)
|
||||
@@ -239,6 +196,46 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow<Animate
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.ChatListWithLoadingScreen(searchText: MutableState<TextFieldValue>, listState: LazyListState) {
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
ChatList(searchText = searchText, listState)
|
||||
}
|
||||
if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
|
||||
Text(
|
||||
stringResource(
|
||||
if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats
|
||||
), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.NewChatSheetFloatingButton(oneHandUI: State<Boolean>, stopped: Boolean) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (!stopped) {
|
||||
showNewChatSheet(oneHandUI)
|
||||
}
|
||||
},
|
||||
Modifier
|
||||
.navigationBarsPadding()
|
||||
.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING)
|
||||
.align(Alignment.BottomEnd)
|
||||
.size(AppBarHeight * fontSizeSqrtMultiplier),
|
||||
elevation = FloatingActionButtonDefaults.elevation(
|
||||
defaultElevation = 0.dp,
|
||||
pressedElevation = 0.dp,
|
||||
hoveredElevation = 0.dp,
|
||||
focusedElevation = 0.dp,
|
||||
),
|
||||
backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
contentColor = Color.White
|
||||
) {
|
||||
Icon(painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(22.dp * fontSizeSqrtMultiplier))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConnectButton(text: String, onClick: () -> Unit) {
|
||||
Button(
|
||||
@@ -256,7 +253,7 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, setPerformLA: (Boolean) -> Unit) {
|
||||
private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>, listState: LazyListState, stopped: Boolean, setPerformLA: (Boolean) -> Unit) {
|
||||
val serversSummary: MutableState<PresentedServersSummary?> = remember { mutableStateOf(null) }
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
val updatingProgress = remember { chatModel.updatingProgress }.value
|
||||
@@ -265,6 +262,18 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
|
||||
if (oneHandUI.value) {
|
||||
val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
|
||||
|
||||
if (appPlatform.isDesktop && oneHandUI.value) {
|
||||
val call = remember { chatModel.activeCall }
|
||||
if (call.value != null) {
|
||||
barButtons.add {
|
||||
val c = call.value
|
||||
if (c != null) {
|
||||
ActiveCallInteractiveArea(c)
|
||||
Spacer(Modifier.width(5.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!stopped) {
|
||||
barButtons.add {
|
||||
IconButton(
|
||||
@@ -323,7 +332,9 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
|
||||
}
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
DefaultTopAppBar(
|
||||
val scope = rememberCoroutineScope()
|
||||
val canScrollToZero = remember { derivedStateOf { listState.firstVisibleItemIndex != 0 || listState.firstVisibleItemScrollOffset != 0 } }
|
||||
DefaultAppBar(
|
||||
navigationButton = {
|
||||
if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) {
|
||||
NavigationButtonMenu {
|
||||
@@ -351,15 +362,14 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
|
||||
SubscriptionStatusIndicator(
|
||||
click = {
|
||||
ModalManager.start.closeModals()
|
||||
val summary = serversSummary.value
|
||||
ModalManager.start.showModalCloseable(
|
||||
endButtons = {
|
||||
val summary = serversSummary.value
|
||||
if (summary != null) {
|
||||
ShareButton {
|
||||
val json = Json {
|
||||
prettyPrint = true
|
||||
}
|
||||
|
||||
val text = json.encodeToString(PresentedServersSummary.serializer(), summary)
|
||||
clipboard.shareText(text)
|
||||
}
|
||||
@@ -370,10 +380,10 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
|
||||
)
|
||||
}
|
||||
},
|
||||
onTitleClick = null,
|
||||
showSearch = false,
|
||||
onTitleClick = if (canScrollToZero.value) { { scrollToBottom(scope, listState) } } else null,
|
||||
onTop = !oneHandUI.value,
|
||||
onSearchValueChanged = {},
|
||||
buttons = barButtons
|
||||
buttons = { barButtons.forEach { it() } }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -491,74 +501,78 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: String, chatModel: ChatModel) {
|
||||
|
||||
@Composable
|
||||
private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Icon(
|
||||
painterResource(MR.images.ic_search),
|
||||
contentDescription = null,
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(22.dp * fontSizeSqrtMultiplier),
|
||||
tint = MaterialTheme.colors.secondary
|
||||
)
|
||||
SearchTextField(
|
||||
Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester),
|
||||
placeholder = stringResource(MR.strings.search_or_paste_simplex_link),
|
||||
alwaysVisible = true,
|
||||
searchText = searchText,
|
||||
enabled = !remember { searchShowingSimplexLink }.value,
|
||||
trailingContent = null,
|
||||
) {
|
||||
searchText.value = searchText.value.copy(it)
|
||||
}
|
||||
val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } }
|
||||
if (hasText.value) {
|
||||
val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() }
|
||||
BackHandler(onBack = hideSearchOnBack)
|
||||
KeyChangeEffect(chatModel.currentRemoteHost.value) {
|
||||
hideSearchOnBack()
|
||||
Box {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Icon(
|
||||
painterResource(MR.images.ic_search),
|
||||
contentDescription = null,
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(22.dp * fontSizeSqrtMultiplier),
|
||||
tint = MaterialTheme.colors.secondary
|
||||
)
|
||||
SearchTextField(
|
||||
Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester),
|
||||
placeholder = stringResource(MR.strings.search_or_paste_simplex_link),
|
||||
alwaysVisible = true,
|
||||
searchText = searchText,
|
||||
enabled = !remember { searchShowingSimplexLink }.value,
|
||||
trailingContent = null,
|
||||
) {
|
||||
searchText.value = searchText.value.copy(it)
|
||||
}
|
||||
} else {
|
||||
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
||||
if (chatModel.chats.value.isNotEmpty()) {
|
||||
ToggleFilterEnabledButton()
|
||||
}
|
||||
Spacer(Modifier.width(padding))
|
||||
}
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboardState = getKeyboardState()
|
||||
LaunchedEffect(keyboardState.value) {
|
||||
if (keyboardState.value == KeyboardState.Closed && focused) {
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
val view = LocalMultiplatformView()
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { searchText.value.text }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
val link = strHasSingleSimplexLink(it.trim())
|
||||
if (link != null) {
|
||||
// if SimpleX link is pasted, show connection dialogue
|
||||
hideKeyboard(view)
|
||||
if (link.format is Format.SimplexLink) {
|
||||
val linkText = link.simplexLinkText(link.format.linkType, link.format.smpHosts)
|
||||
searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero)
|
||||
}
|
||||
searchShowingSimplexLink.value = true
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
connect(link.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() }
|
||||
} else if (!searchShowingSimplexLink.value || it.isEmpty()) {
|
||||
if (it.isNotEmpty()) {
|
||||
// if some other text is pasted, enter search mode
|
||||
focusRequester.requestFocus()
|
||||
} else if (listState.layoutInfo.totalItemsCount > 0) {
|
||||
listState.scrollToItem(0)
|
||||
}
|
||||
searchShowingSimplexLink.value = false
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
}
|
||||
val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } }
|
||||
if (hasText.value) {
|
||||
val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() }
|
||||
BackHandler(onBack = hideSearchOnBack)
|
||||
KeyChangeEffect(chatModel.currentRemoteHost.value) {
|
||||
hideSearchOnBack()
|
||||
}
|
||||
} else {
|
||||
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
||||
if (chatModel.chats.value.isNotEmpty()) {
|
||||
ToggleFilterEnabledButton()
|
||||
}
|
||||
Spacer(Modifier.width(padding))
|
||||
}
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboardState = getKeyboardState()
|
||||
LaunchedEffect(keyboardState.value) {
|
||||
if (keyboardState.value == KeyboardState.Closed && focused) {
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
val view = LocalMultiplatformView()
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { searchText.value.text }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
val link = strHasSingleSimplexLink(it.trim())
|
||||
if (link != null) {
|
||||
// if SimpleX link is pasted, show connection dialogue
|
||||
hideKeyboard(view)
|
||||
if (link.format is Format.SimplexLink) {
|
||||
val linkText = link.simplexLinkText(link.format.linkType, link.format.smpHosts)
|
||||
searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero)
|
||||
}
|
||||
searchShowingSimplexLink.value = true
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
connect(link.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() }
|
||||
} else if (!searchShowingSimplexLink.value || it.isEmpty()) {
|
||||
if (it.isNotEmpty()) {
|
||||
// if some other text is pasted, enter search mode
|
||||
focusRequester.requestFocus()
|
||||
} else if (listState.layoutInfo.totalItemsCount > 0) {
|
||||
listState.scrollToItem(0)
|
||||
}
|
||||
searchShowingSimplexLink.value = false
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
Divider(Modifier.align(if (oneHandUI.value) Alignment.TopStart else Alignment.BottomStart))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,8 +604,37 @@ enum class ScrollDirection {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldValue>) {
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
fun BoxScope.StatusBarBackground() {
|
||||
if (appPlatform.isAndroid) {
|
||||
val finalColor = MaterialTheme.colors.background.copy(0.88f)
|
||||
Box(Modifier.fillMaxWidth().windowInsetsTopHeight(WindowInsets.statusBars).background(finalColor))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BoxScope.NavigationBarBackground(appBarOnBottom: Boolean = false, mixedColor: Boolean, noAlpha: Boolean = false) {
|
||||
if (appPlatform.isAndroid) {
|
||||
val barPadding = WindowInsets.navigationBars.asPaddingValues()
|
||||
val paddingBottom = barPadding.calculateBottomPadding()
|
||||
val color = if (mixedColor) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) else MaterialTheme.colors.background
|
||||
val finalColor = color.copy(if (noAlpha) 1f else if (appBarOnBottom) remember { appPrefs.inAppBarsAlpha.state }.value else 0.6f)
|
||||
Box(Modifier.align(Alignment.BottomStart).height(paddingBottom).fillMaxWidth().background(finalColor))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BoxScope.NavigationBarBackground(modifier: Modifier, color: Color = MaterialTheme.colors.background) {
|
||||
val keyboardState = getKeyboardState()
|
||||
if (appPlatform.isAndroid && keyboardState.value == KeyboardState.Closed) {
|
||||
val barPadding = WindowInsets.navigationBars.asPaddingValues()
|
||||
val paddingBottom = barPadding.calculateBottomPadding()
|
||||
val finalColor = color.copy(0.6f)
|
||||
Box(modifier.align(Alignment.BottomStart).height(paddingBottom).fillMaxWidth().background(finalColor))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listState: LazyListState) {
|
||||
var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) }
|
||||
var previousIndex by remember { mutableStateOf(0) }
|
||||
var previousScrollOffset by remember { mutableStateOf(0) }
|
||||
@@ -628,40 +671,45 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
||||
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
||||
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
|
||||
val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList())
|
||||
val topPaddingToContent = topPaddingToContent()
|
||||
val blankSpaceSize = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else topPaddingToContent
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
if (!oneHandUI.value) Modifier.imePadding() else Modifier,
|
||||
listState,
|
||||
reverseLayout = oneHandUI.value
|
||||
) {
|
||||
item { Spacer(Modifier.height(blankSpaceSize)) }
|
||||
stickyHeader {
|
||||
Column(
|
||||
Modifier
|
||||
.zIndex(1f)
|
||||
.offset {
|
||||
val y = if (searchText.value.text.isEmpty()) {
|
||||
val offsetMultiplier = if (oneHandUI.value) 1 else -1
|
||||
if (
|
||||
(oneHandUI.value && scrollDirection == ScrollDirection.Up) ||
|
||||
(appPlatform.isAndroid && keyboardState == KeyboardState.Opened)
|
||||
) {
|
||||
0
|
||||
} else if (listState.firstVisibleItemIndex == 0) offsetMultiplier * listState.firstVisibleItemScrollOffset else offsetMultiplier * 1000
|
||||
val offsetMultiplier = if (oneHandUI.value) 1 else -1
|
||||
val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened) || scrollDirection == ScrollDirection.Up) {
|
||||
if (listState.firstVisibleItemIndex == 0) -offsetMultiplier * listState.firstVisibleItemScrollOffset
|
||||
else -offsetMultiplier * blankSpaceSize.roundToPx()
|
||||
} else {
|
||||
0
|
||||
when (listState.firstVisibleItemIndex) {
|
||||
0 -> 0
|
||||
1 -> offsetMultiplier * listState.firstVisibleItemScrollOffset
|
||||
else -> offsetMultiplier * 1000
|
||||
}
|
||||
}
|
||||
IntOffset(0, y)
|
||||
}
|
||||
.background(MaterialTheme.colors.background),
|
||||
.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
if (oneHandUI.value) {
|
||||
Divider()
|
||||
}
|
||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
|
||||
if (!oneHandUI.value) {
|
||||
Divider()
|
||||
Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) {
|
||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
|
||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
|
||||
}
|
||||
} else {
|
||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (appPlatform.isAndroid && !oneHandUICardShown.value && chats.count() > 1) {
|
||||
if (!oneHandUICardShown.value && chats.size > 1) {
|
||||
item {
|
||||
ToggleChatListCard()
|
||||
}
|
||||
@@ -672,17 +720,30 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
||||
} }
|
||||
ChatListNavLinkView(chat, nextChatSelected)
|
||||
}
|
||||
if (appPlatform.isAndroid && !oneHandUICardShown.value && chats.count() <= 1) {
|
||||
if (!oneHandUICardShown.value && chats.size <= 1) {
|
||||
item {
|
||||
ToggleChatListCard()
|
||||
}
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
item { Spacer(if (oneHandUI.value) Modifier.windowInsetsTopHeight(WindowInsets.statusBars) else Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) }
|
||||
}
|
||||
}
|
||||
if (chats.isEmpty() && chatModel.chats.value.isNotEmpty()) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Box(Modifier.fillMaxSize().imePadding(), contentAlignment = Alignment.Center) {
|
||||
Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
if (oneHandUI.value) {
|
||||
StatusBarBackground()
|
||||
} else {
|
||||
NavigationBarBackground(oneHandUI.value, true)
|
||||
}
|
||||
if (!oneHandUICardShown.value) {
|
||||
LaunchedEffect(chats.size) {
|
||||
if (chats.size >= 3) appPrefs.oneHandUICardShown.set(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun filteredChats(
|
||||
@@ -727,3 +788,7 @@ private fun filtered(chat: Chat): Boolean =
|
||||
(chat.chatInfo.chatSettings?.favorite ?: false) ||
|
||||
chat.chatStats.unreadChat ||
|
||||
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
|
||||
|
||||
fun scrollToBottom(scope: CoroutineScope, listState: LazyListState) {
|
||||
scope.launch { try { listState.animateScrollToItem(0) } catch (e: Exception) { Log.e(TAG, e.stackTraceToString()) } }
|
||||
}
|
||||
|
||||
+5
-15
@@ -620,9 +620,7 @@ fun ModalData.SMPServerSummaryView(
|
||||
ModalView(
|
||||
close = close
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.smp_server),
|
||||
@@ -645,9 +643,7 @@ fun ModalData.DetailedXFTPStatsView(
|
||||
ModalView(
|
||||
close = close
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
@@ -671,9 +667,7 @@ fun ModalData.DetailedSMPStatsView(
|
||||
ModalView(
|
||||
close = close
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
@@ -697,9 +691,7 @@ fun ModalData.XFTPServerSummaryView(
|
||||
ModalView(
|
||||
close = close
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
@@ -715,9 +707,7 @@ fun ModalData.XFTPServerSummaryView(
|
||||
|
||||
@Composable
|
||||
fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableState<PresentedServersSummary?>) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
var showUserSelection by remember { mutableStateOf(false) }
|
||||
val selectedUserCategory =
|
||||
remember { stateGetOrPut("selectedUserCategory") { PresentedUserCategory.ALL_USERS } }
|
||||
|
||||
+55
-67
@@ -11,10 +11,13 @@ import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.themedBackground
|
||||
import chat.simplex.common.views.chat.topPaddingToContent
|
||||
import chat.simplex.common.views.newchat.ActiveProfilePicker
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@@ -22,26 +25,7 @@ import chat.simplex.res.MR
|
||||
fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
|
||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
|
||||
Scaffold(
|
||||
contentColor = LocalContentColor.current,
|
||||
topBar = {
|
||||
if (!oneHandUI.value) {
|
||||
Column {
|
||||
ShareListToolbar(chatModel, stopped) { searchInList = it.trim() }
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
if (oneHandUI.value) {
|
||||
Column {
|
||||
Divider()
|
||||
ShareListToolbar(chatModel, stopped) { searchInList = it.trim() }
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Box(Modifier.fillMaxSize().themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) {
|
||||
val sharedContent = chatModel.sharedContent.value
|
||||
var isMediaOrFileAttachment = false
|
||||
var isVoice = false
|
||||
@@ -69,22 +53,24 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
Box(Modifier.padding(it)) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
if (chatModel.chats.value.isNotEmpty()) {
|
||||
ShareList(
|
||||
chatModel,
|
||||
search = searchInList,
|
||||
isMediaOrFileAttachment = isMediaOrFileAttachment,
|
||||
isVoice = isVoice,
|
||||
hasSimplexLink = hasSimplexLink,
|
||||
)
|
||||
} else {
|
||||
EmptyList()
|
||||
}
|
||||
}
|
||||
if (chatModel.chats.value.isNotEmpty()) {
|
||||
ShareList(
|
||||
chatModel,
|
||||
search = searchInList,
|
||||
isMediaOrFileAttachment = isMediaOrFileAttachment,
|
||||
isVoice = isVoice,
|
||||
hasSimplexLink = hasSimplexLink,
|
||||
)
|
||||
} else {
|
||||
EmptyList()
|
||||
}
|
||||
if (oneHandUI.value) {
|
||||
StatusBarBackground()
|
||||
} else {
|
||||
NavigationBarBackground(oneHandUI.value, true)
|
||||
}
|
||||
Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) {
|
||||
ShareListToolbar(chatModel, stopped) { searchInList = it.trim() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,7 +94,6 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
|
||||
if (showSearch) {
|
||||
BackHandler(onBack = hideSearchOnBack)
|
||||
}
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
|
||||
val navButton: @Composable RowScope.() -> Unit = {
|
||||
when {
|
||||
@@ -118,13 +103,13 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
|
||||
.filter { u -> !u.user.activeUser && !u.user.hidden }
|
||||
.all { u -> u.unreadCount == 0 }
|
||||
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
ModalManager.start.showCustomModal(keyboardCoversBar = false) { close ->
|
||||
val search = rememberSaveable { mutableStateOf("") }
|
||||
ModalView(
|
||||
{ close() },
|
||||
endButtons = {
|
||||
SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
|
||||
},
|
||||
showSearch = true,
|
||||
searchAlwaysVisible = true,
|
||||
onSearchValueChanged = { search.value = it },
|
||||
content = {
|
||||
ActiveProfilePicker(
|
||||
search = search,
|
||||
@@ -148,31 +133,8 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
|
||||
})
|
||||
}
|
||||
}
|
||||
if (chatModel.chats.value.size >= 8) {
|
||||
barButtons.add {
|
||||
IconButton({ showSearch = true }) {
|
||||
Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stopped) {
|
||||
barButtons.add {
|
||||
IconButton(onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.chat_is_stopped_indication),
|
||||
generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app)
|
||||
)
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_report_filled),
|
||||
generalGetString(MR.strings.chat_is_stopped_indication),
|
||||
tint = Color.Red,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DefaultTopAppBar(
|
||||
DefaultAppBar(
|
||||
navigationButton = navButton,
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -191,8 +153,29 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
|
||||
},
|
||||
onTitleClick = null,
|
||||
showSearch = showSearch,
|
||||
onTop = !remember { appPrefs.oneHandUI.state }.value,
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
buttons = barButtons
|
||||
buttons = {
|
||||
if (chatModel.chats.value.size >= 8) {
|
||||
IconButton({ showSearch = true }) {
|
||||
Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
if (stopped) {
|
||||
IconButton(onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.chat_is_stopped_indication),
|
||||
generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app)
|
||||
)
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_report_filled),
|
||||
generalGetString(MR.strings.chat_is_stopped_indication),
|
||||
tint = Color.Red,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -211,8 +194,13 @@ private fun ShareList(
|
||||
filteredChats(false, mutableStateOf(false), mutableStateOf(null), search, sorted)
|
||||
}
|
||||
}
|
||||
val topPaddingToContent = topPaddingToContent()
|
||||
LazyColumnWithScrollBar(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier.then(if (oneHandUI.value) Modifier.consumeWindowInsets(WindowInsets.navigationBars.only(WindowInsetsSides.Vertical)) else Modifier).imePadding(),
|
||||
contentPadding = PaddingValues(
|
||||
top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else topPaddingToContent,
|
||||
bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp
|
||||
),
|
||||
reverseLayout = oneHandUI.value
|
||||
) {
|
||||
items(chats) { chat ->
|
||||
|
||||
+16
-6
@@ -23,6 +23,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.ui.theme.*
|
||||
@@ -137,12 +138,16 @@ fun UserPicker(
|
||||
}
|
||||
}
|
||||
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val iconColor = MaterialTheme.colors.secondaryVariant
|
||||
val background = if (appPlatform.isAndroid) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha()) else MaterialTheme.colors.surface
|
||||
PlatformUserPicker(
|
||||
modifier = Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth()
|
||||
.then(if (newChat.isVisible()) Modifier.shadow(8.dp, clip = true) else Modifier)
|
||||
.background(if (appPlatform.isAndroid) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha()) else MaterialTheme.colors.surface)
|
||||
.then(if (newChat.isVisible()) Modifier.shadow(8.dp, clip = true, ambientColor = background) else Modifier)
|
||||
.padding(top = if (appPlatform.isDesktop && oneHandUI.value) 7.dp else 0.dp)
|
||||
.background(background)
|
||||
.padding(bottom = USER_PICKER_SECTION_SPACING - DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL),
|
||||
pickerState = userPickerState
|
||||
) {
|
||||
@@ -198,12 +203,13 @@ fun UserPicker(
|
||||
UserPickerUsersSection(
|
||||
users = users,
|
||||
onUserClicked = onUserClicked,
|
||||
iconColor = iconColor,
|
||||
stopped = stopped
|
||||
)
|
||||
}
|
||||
} else if (currentUser != null) {
|
||||
SectionItemView({ onUserClicked(currentUser) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) {
|
||||
ProfilePreview(currentUser.profile, stopped = stopped)
|
||||
ProfilePreview(currentUser.profile, iconColor = iconColor, stopped = stopped)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,6 +240,7 @@ fun UserPicker(
|
||||
Column(modifier = Modifier.padding(vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL)) {
|
||||
UserPickerUsersSection(
|
||||
users = inactiveUsers,
|
||||
iconColor = iconColor,
|
||||
onUserClicked = onUserClicked,
|
||||
stopped = stopped
|
||||
)
|
||||
@@ -265,13 +272,15 @@ fun UserPicker(
|
||||
generalGetString(MR.strings.auth_open_chat_profiles),
|
||||
generalGetString(MR.strings.auth_log_in_using_credential)
|
||||
) {
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
ModalManager.start.showCustomModal(keyboardCoversBar = false) { close ->
|
||||
val search = rememberSaveable { mutableStateOf("") }
|
||||
val profileHidden = rememberSaveable { mutableStateOf(false) }
|
||||
ModalView(
|
||||
{ close() },
|
||||
endButtons = {
|
||||
SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
|
||||
showSearch = true,
|
||||
searchAlwaysVisible = true,
|
||||
onSearchValueChanged = {
|
||||
search.value = it
|
||||
},
|
||||
content = { UserProfilesView(chatModel, search, profileHidden) })
|
||||
}
|
||||
@@ -519,6 +528,7 @@ private fun DevicePickerRow(
|
||||
@Composable
|
||||
expect fun UserPickerUsersSection(
|
||||
users: List<UserInfo>,
|
||||
iconColor: Color,
|
||||
stopped: Boolean,
|
||||
onUserClicked: (user: User) -> Unit,
|
||||
)
|
||||
|
||||
+1
-3
@@ -46,9 +46,7 @@ fun ChatArchiveLayout(
|
||||
saveArchive: () -> Unit,
|
||||
deleteArchiveAlert: () -> Unit
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(title)
|
||||
SectionView(stringResource(MR.strings.chat_archive_section)) {
|
||||
SettingsActionItem(
|
||||
|
||||
+1
-1
@@ -203,7 +203,7 @@ fun DatabaseEncryptionLayout(
|
||||
Layout()
|
||||
}
|
||||
} else {
|
||||
ColumnWithScrollBar(Modifier.fillMaxWidth(), maxIntrinsicSize = true) {
|
||||
ColumnWithScrollBar(maxIntrinsicSize = true) {
|
||||
Layout()
|
||||
}
|
||||
}
|
||||
|
||||
+1
-4
@@ -77,10 +77,7 @@ fun DatabaseErrorView(
|
||||
Text(String.format(generalGetString(MR.strings.database_migrations), ms.joinToString(", ")))
|
||||
}
|
||||
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
ColumnWithScrollBarNoAppBar(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
|
||||
val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value
|
||||
when (val status = chatDbStatus.value) {
|
||||
is DBMigrationResult.ErrorNotADatabase ->
|
||||
|
||||
+1
-3
@@ -156,9 +156,7 @@ fun DatabaseLayout(
|
||||
val stopped = !runChat
|
||||
val operationsDisabled = (!stopped || progressIndicator) && !chatModel.desktopNoUserNoRemote
|
||||
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.your_chat_database))
|
||||
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@Composable
|
||||
fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f + 8.dp) {
|
||||
val handler = LocalAppBarHandler.current
|
||||
val connection = handler?.connection
|
||||
LaunchedEffect(title) {
|
||||
handler?.title?.value = title
|
||||
}
|
||||
val theme = CurrentColors.collectAsState()
|
||||
val titleColor = MaterialTheme.appColors.title
|
||||
val brush = if (theme.value.base == DefaultTheme.SIMPLEX)
|
||||
Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
|
||||
else // color is not updated when changing themes if I pass null here
|
||||
Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
|
||||
Column {
|
||||
Text(
|
||||
title,
|
||||
Modifier
|
||||
.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, top = DEFAULT_PADDING_HALF, end = if (withPadding) DEFAULT_PADDING else 0.dp,)
|
||||
.graphicsLayer {
|
||||
alpha = bottomTitleAlpha(connection)
|
||||
},
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.h1.copy(brush = brush),
|
||||
color = MaterialTheme.colors.primaryVariant,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
if (hostDevice != null) {
|
||||
Box(Modifier.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp).graphicsLayer {
|
||||
alpha = bottomTitleAlpha(connection)
|
||||
}) {
|
||||
HostDeviceTitle(hostDevice)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(bottomPadding))
|
||||
}
|
||||
}
|
||||
|
||||
private fun bottomTitleAlpha(connection: CollapsingAppBarNestedScrollConnection?) =
|
||||
if ((connection?.appBarOffset ?: 0f).absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 1f
|
||||
else ((AppBarHandler.appBarMaxHeightPx) + (connection?.appBarOffset ?: 0f) / 1.5f).coerceAtLeast(0f) / AppBarHandler.appBarMaxHeightPx
|
||||
|
||||
@Composable
|
||||
private fun HostDeviceTitle(hostDevice: Pair<Long?, String>, extraPadding: Boolean = false) {
|
||||
Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) {
|
||||
DevicePill(
|
||||
active = true,
|
||||
onClick = {},
|
||||
actionButtonVisible = false,
|
||||
icon = painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300),
|
||||
text = hostDevice.second
|
||||
)
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||
import androidx.compose.ui.graphics.drawscope.translate
|
||||
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||
import androidx.compose.ui.graphics.layer.drawLayer
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
|
||||
fun Modifier.blurredBackgroundModifier(
|
||||
keyboardInset: WindowInsets,
|
||||
handler: AppBarHandler?,
|
||||
blurRadius: State<Int>,
|
||||
prefAlpha: State<Float>,
|
||||
keyboardCoversBar: Boolean,
|
||||
onTop: Boolean,
|
||||
density: Density
|
||||
): Modifier {
|
||||
val graphicsLayer = handler?.graphicsLayer
|
||||
val backgroundGraphicsLayer = handler?.backgroundGraphicsLayer
|
||||
val backgroundGraphicsLayerSize = handler?.backgroundGraphicsLayerSize
|
||||
if (handler == null || graphicsLayer == null || backgroundGraphicsLayer == null || blurRadius.value == 0 || prefAlpha.value == 1f || backgroundGraphicsLayerSize === null)
|
||||
return this
|
||||
|
||||
return if (appPlatform.isAndroid) {
|
||||
this.androidBlurredModifier(keyboardInset, blurRadius.value, keyboardCoversBar, onTop, graphicsLayer, backgroundGraphicsLayer, backgroundGraphicsLayerSize, density)
|
||||
} else {
|
||||
this.desktopBlurredModifier(keyboardInset, blurRadius, keyboardCoversBar, onTop, graphicsLayer, backgroundGraphicsLayer, backgroundGraphicsLayerSize, density)
|
||||
}
|
||||
}
|
||||
|
||||
// this is more performant version than for Android but can't be used on desktop because on first frame it shows transparent view
|
||||
// which is very noticeable on desktop and unnoticeable on Android
|
||||
private fun Modifier.androidBlurredModifier(
|
||||
keyboardInset: WindowInsets,
|
||||
blurRadius: Int,
|
||||
keyboardCoversBar: Boolean,
|
||||
onTop: Boolean,
|
||||
graphicsLayer: GraphicsLayer,
|
||||
backgroundGraphicsLayer: GraphicsLayer,
|
||||
backgroundGraphicsLayerSize: State<IntSize>,
|
||||
density: Density
|
||||
): Modifier = this
|
||||
.graphicsLayer {
|
||||
renderEffect = if (blurRadius > 0) BlurEffect(blurRadius.dp.toPx(), blurRadius.dp.toPx()) else null
|
||||
clip = blurRadius > 0
|
||||
}
|
||||
.graphicsLayer {
|
||||
if (!onTop) {
|
||||
val bgSize = when {
|
||||
backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height
|
||||
backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height
|
||||
else -> backgroundGraphicsLayerSize.value.height
|
||||
}
|
||||
val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0
|
||||
translationY = -bgSize + size.height + keyboardHeightCovered
|
||||
}
|
||||
}
|
||||
.drawBehind {
|
||||
drawRect(Color.Black)
|
||||
if (onTop) {
|
||||
clipRect {
|
||||
if (backgroundGraphicsLayer.size != IntSize.Zero) {
|
||||
drawLayer(backgroundGraphicsLayer)
|
||||
} else {
|
||||
drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat()))
|
||||
}
|
||||
drawLayer(graphicsLayer)
|
||||
}
|
||||
} else {
|
||||
if (backgroundGraphicsLayer.size != IntSize.Zero) {
|
||||
drawLayer(backgroundGraphicsLayer)
|
||||
} else {
|
||||
drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat()))
|
||||
}
|
||||
drawLayer(graphicsLayer)
|
||||
}
|
||||
}
|
||||
.graphicsLayer {
|
||||
if (!onTop) {
|
||||
val bgSize = when {
|
||||
backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height
|
||||
backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height
|
||||
else -> backgroundGraphicsLayerSize.value.height
|
||||
}
|
||||
val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0
|
||||
translationY -= -bgSize + size.height + keyboardHeightCovered
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.desktopBlurredModifier(
|
||||
keyboardInset: WindowInsets,
|
||||
blurRadius: State<Int>,
|
||||
keyboardCoversBar: Boolean,
|
||||
onTop: Boolean,
|
||||
graphicsLayer: GraphicsLayer,
|
||||
backgroundGraphicsLayer: GraphicsLayer,
|
||||
backgroundGraphicsLayerSize: State<IntSize>,
|
||||
density: Density
|
||||
): Modifier = this
|
||||
.graphicsLayer {
|
||||
renderEffect = if (blurRadius.value > 0) BlurEffect(blurRadius.value.dp.toPx(), blurRadius.value.dp.toPx()) else null
|
||||
clip = blurRadius.value > 0
|
||||
}
|
||||
.drawBehind {
|
||||
drawRect(Color.Black)
|
||||
if (onTop) {
|
||||
clipRect {
|
||||
if (backgroundGraphicsLayer.size != IntSize.Zero) {
|
||||
drawLayer(backgroundGraphicsLayer)
|
||||
} else {
|
||||
drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat()))
|
||||
}
|
||||
drawLayer(graphicsLayer)
|
||||
}
|
||||
} else {
|
||||
val bgSize = when {
|
||||
backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height
|
||||
backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height
|
||||
else -> backgroundGraphicsLayerSize.value.height
|
||||
}
|
||||
val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0
|
||||
translate(top = -bgSize + size.height + keyboardHeightCovered) {
|
||||
if (backgroundGraphicsLayer.size != IntSize.Zero) {
|
||||
drawLayer(backgroundGraphicsLayer)
|
||||
} else {
|
||||
drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat()))
|
||||
}
|
||||
drawLayer(graphicsLayer)
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
-41
@@ -1,11 +1,13 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.draw.CacheDrawScope
|
||||
import androidx.compose.ui.draw.DrawResult
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.*
|
||||
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
@@ -381,7 +383,14 @@ private fun drawToBitmap(image: ImageBitmap, imageScale: Float, tint: Color, siz
|
||||
return bitmap
|
||||
}
|
||||
|
||||
fun CacheDrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperType, background: Color, tint: Color): DrawResult {
|
||||
fun CacheDrawScope.chatViewBackground(
|
||||
image: ImageBitmap,
|
||||
imageType: WallpaperType,
|
||||
background: Color,
|
||||
tint: Color,
|
||||
graphicsLayerSize: MutableState<IntSize>? = null,
|
||||
backgroundGraphicsLayer: GraphicsLayer? = null
|
||||
): DrawResult {
|
||||
val imageScale = if (imageType is WallpaperType.Preset) {
|
||||
(imageType.scale ?: 1f) * imageType.predefinedImageScale
|
||||
} else if (imageType is WallpaperType.Image && imageType.scaleType == WallpaperScaleType.REPEAT) {
|
||||
@@ -396,53 +405,55 @@ fun CacheDrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperTy
|
||||
}
|
||||
|
||||
return onDrawBehind {
|
||||
val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low
|
||||
drawRect(background)
|
||||
when (imageType) {
|
||||
is WallpaperType.Preset -> drawImage(image)
|
||||
is WallpaperType.Image -> when (val scaleType = imageType.scaleType ?: WallpaperScaleType.FILL) {
|
||||
WallpaperScaleType.REPEAT -> drawImage(image)
|
||||
WallpaperScaleType.FILL, WallpaperScaleType.FIT -> {
|
||||
clipRect {
|
||||
val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height))
|
||||
val scaledWidth = (image.width * scale.scaleX).roundToInt()
|
||||
val scaledHeight = (image.height * scale.scaleY).roundToInt()
|
||||
// Large image will cause freeze
|
||||
if (image.width > 4320 || image.height > 4320) return@clipRect
|
||||
copyBackgroundToAppBar(graphicsLayerSize, backgroundGraphicsLayer) {
|
||||
val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low
|
||||
drawRect(background)
|
||||
when (imageType) {
|
||||
is WallpaperType.Preset -> drawImage(image)
|
||||
is WallpaperType.Image -> when (val scaleType = imageType.scaleType ?: WallpaperScaleType.FILL) {
|
||||
WallpaperScaleType.REPEAT -> drawImage(image)
|
||||
WallpaperScaleType.FILL, WallpaperScaleType.FIT -> {
|
||||
clipRect {
|
||||
val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height))
|
||||
val scaledWidth = (image.width * scale.scaleX).roundToInt()
|
||||
val scaledHeight = (image.height * scale.scaleY).roundToInt()
|
||||
// Large image will cause freeze
|
||||
if (image.width > 4320 || image.height > 4320) return@clipRect
|
||||
|
||||
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
if (scaleType == WallpaperScaleType.FIT) {
|
||||
if (scaledWidth < size.width) {
|
||||
// has black lines at left and right sides
|
||||
var x = (size.width - scaledWidth) / 2
|
||||
while (x > 0) {
|
||||
drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
x -= scaledWidth
|
||||
}
|
||||
x = size.width - (size.width - scaledWidth) / 2
|
||||
while (x < size.width) {
|
||||
drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
x += scaledWidth
|
||||
}
|
||||
} else {
|
||||
// has black lines at top and bottom sides
|
||||
var y = (size.height - scaledHeight) / 2
|
||||
while (y > 0) {
|
||||
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
y -= scaledHeight
|
||||
}
|
||||
y = size.height - (size.height - scaledHeight) / 2
|
||||
while (y < size.height) {
|
||||
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
y += scaledHeight
|
||||
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
if (scaleType == WallpaperScaleType.FIT) {
|
||||
if (scaledWidth < size.width) {
|
||||
// has black lines at left and right sides
|
||||
var x = (size.width - scaledWidth) / 2
|
||||
while (x > 0) {
|
||||
drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
x -= scaledWidth
|
||||
}
|
||||
x = size.width - (size.width - scaledWidth) / 2
|
||||
while (x < size.width) {
|
||||
drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
x += scaledWidth
|
||||
}
|
||||
} else {
|
||||
// has black lines at top and bottom sides
|
||||
var y = (size.height - scaledHeight) / 2
|
||||
while (y > 0) {
|
||||
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
y -= scaledHeight
|
||||
}
|
||||
y = size.height - (size.height - scaledHeight) / 2
|
||||
while (y < size.height) {
|
||||
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
y += scaledHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
drawRect(tint)
|
||||
}
|
||||
drawRect(tint)
|
||||
}
|
||||
is WallpaperType.Empty -> {}
|
||||
}
|
||||
is WallpaperType.Empty -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -19,6 +19,8 @@ fun ChooseAttachmentView(attachmentOption: MutableState<AttachmentOption?>, hide
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.imePadding()
|
||||
.wrapContentHeight()
|
||||
.onFocusChanged { focusState ->
|
||||
if (!focusState.hasFocus) hide()
|
||||
|
||||
-181
@@ -1,181 +0,0 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.ui.draw.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chatlist.DevicePill
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@Composable
|
||||
fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Color = if (close != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, arrangement: Arrangement.Vertical = Arrangement.Top, closeBarTitle: String? = null, barPaddingValues: PaddingValues = PaddingValues(horizontal = AppBarHorizontalPadding), endButtons: @Composable RowScope.() -> Unit = {}) {
|
||||
var rowModifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(AppBarHeight * fontSizeSqrtMultiplier)
|
||||
val themeBackgroundMix = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f)
|
||||
if (!closeBarTitle.isNullOrEmpty()) {
|
||||
rowModifier = rowModifier.background(themeBackgroundMix)
|
||||
}
|
||||
val handler = LocalAppBarHandler.current
|
||||
val connection = LocalAppBarHandler.current?.connection
|
||||
val title = remember(handler?.title?.value) { handler?.title ?: mutableStateOf("") }
|
||||
|
||||
Column(
|
||||
verticalArrangement = arrangement,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = AppBarHeight * fontSizeSqrtMultiplier)
|
||||
.drawWithCache {
|
||||
val backgroundColor = if (appPlatform.isDesktop && connection != null) themeBackgroundMix.copy(alpha = topTitleAlpha(connection)) else Color.Transparent
|
||||
onDrawBehind {
|
||||
if (appPlatform.isDesktop) {
|
||||
drawRect(backgroundColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(barPaddingValues),
|
||||
content = {
|
||||
Row(
|
||||
rowModifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (showClose) {
|
||||
NavigationButtonBack(tintColor = tintColor, onButtonClicked = close)
|
||||
} else {
|
||||
Spacer(Modifier)
|
||||
}
|
||||
if (!closeBarTitle.isNullOrEmpty()) {
|
||||
Row(
|
||||
Modifier.weight(1f),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
closeBarTitle,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
} else if (title.value.isNotEmpty() && connection != null) {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(start = if (showClose) 0.dp else DEFAULT_PADDING_HALF)
|
||||
.weight(1f) // hides the title if something wants full width (eg, search field in chat profiles screen)
|
||||
.graphicsLayer {
|
||||
alpha = topTitleAlpha((connection))
|
||||
}
|
||||
.padding(start = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
title.value,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Spacer(Modifier.weight(1f))
|
||||
}
|
||||
Row {
|
||||
endButtons()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
if (closeBarTitle.isNullOrEmpty() && title.value.isNotEmpty() && connection != null) {
|
||||
Divider(
|
||||
Modifier
|
||||
.graphicsLayer {
|
||||
alpha = topTitleAlpha(connection)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f + 8.dp) {
|
||||
val handler = LocalAppBarHandler.current
|
||||
val connection = handler?.connection
|
||||
LaunchedEffect(title) {
|
||||
handler?.title?.value = title
|
||||
}
|
||||
val theme = CurrentColors.collectAsState()
|
||||
val titleColor = MaterialTheme.appColors.title
|
||||
val brush = if (theme.value.base == DefaultTheme.SIMPLEX)
|
||||
Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
|
||||
else // color is not updated when changing themes if I pass null here
|
||||
Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
|
||||
Column {
|
||||
Text(
|
||||
title,
|
||||
Modifier
|
||||
.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, top = DEFAULT_PADDING_HALF, end = if (withPadding) DEFAULT_PADDING else 0.dp,)
|
||||
.graphicsLayer {
|
||||
alpha = bottomTitleAlpha(connection)
|
||||
},
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.h1.copy(brush = brush),
|
||||
color = MaterialTheme.colors.primaryVariant,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
if (hostDevice != null) {
|
||||
Box(Modifier.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp).graphicsLayer {
|
||||
alpha = bottomTitleAlpha(connection)
|
||||
}) {
|
||||
HostDeviceTitle(hostDevice)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(bottomPadding))
|
||||
}
|
||||
}
|
||||
|
||||
private fun topTitleAlpha(connection: CollapsingAppBarNestedScrollConnection) =
|
||||
if (connection.appBarOffset.absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 0f
|
||||
else ((-connection.appBarOffset * 1.5f) / (AppBarHandler.appBarMaxHeightPx)).coerceIn(0f, 1f)
|
||||
|
||||
private fun bottomTitleAlpha(connection: CollapsingAppBarNestedScrollConnection?) =
|
||||
if ((connection?.appBarOffset ?: 0f).absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 1f
|
||||
else ((AppBarHandler.appBarMaxHeightPx) + (connection?.appBarOffset ?: 0f) / 1.5f).coerceAtLeast(0f) / AppBarHandler.appBarMaxHeightPx
|
||||
|
||||
@Composable
|
||||
private fun HostDeviceTitle(hostDevice: Pair<Long?, String>, extraPadding: Boolean = false) {
|
||||
Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) {
|
||||
DevicePill(
|
||||
active = true,
|
||||
onClick = {},
|
||||
actionButtonVisible = false,
|
||||
icon = painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300),
|
||||
text = hostDevice.second
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview/*(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)*/
|
||||
@Composable
|
||||
fun PreviewCloseSheetBar() {
|
||||
SimpleXTheme {
|
||||
CloseSheetBar(close = {})
|
||||
}
|
||||
}
|
||||
+55
-1
@@ -3,15 +3,67 @@ package chat.simplex.common.views.helpers
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.*
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||
import androidx.compose.ui.graphics.layer.drawLayer
|
||||
import androidx.compose.ui.graphics.rememberGraphicsLayer
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
|
||||
val LocalAppBarHandler: ProvidableCompositionLocal<AppBarHandler?> = staticCompositionLocalOf { null }
|
||||
|
||||
@Composable
|
||||
fun rememberAppBarHandler(key1: Any? = null, key2: Any? = null, keyboardCoversBar: Boolean = true): AppBarHandler {
|
||||
val graphicsLayer = rememberGraphicsLayer()
|
||||
val backgroundGraphicsLayer = rememberGraphicsLayer()
|
||||
return remember(key1, key2) { AppBarHandler(graphicsLayer, backgroundGraphicsLayer, keyboardCoversBar) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun adjustAppBarHandler(handler: AppBarHandler): AppBarHandler {
|
||||
val graphicsLayer = rememberGraphicsLayer()
|
||||
val backgroundGraphicsLayer = rememberGraphicsLayer()
|
||||
if (handler.graphicsLayer == null || handler.graphicsLayer?.isReleased == true || handler.backgroundGraphicsLayer?.isReleased == true) {
|
||||
handler.graphicsLayer = graphicsLayer
|
||||
handler.backgroundGraphicsLayer = backgroundGraphicsLayer
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
fun Modifier.copyViewToAppBar(blurRadius: Int, graphicsLayer: GraphicsLayer?): Modifier {
|
||||
return if (blurRadius > 0 && graphicsLayer != null) {
|
||||
this.drawWithContent {
|
||||
graphicsLayer.record {
|
||||
this@drawWithContent.drawContent()
|
||||
}
|
||||
drawLayer(graphicsLayer)
|
||||
}
|
||||
} else this
|
||||
}
|
||||
|
||||
fun DrawScope.copyBackgroundToAppBar(graphicsLayerSize: MutableState<IntSize>?, backgroundGraphicsLayer: GraphicsLayer?, scope: DrawScope.() -> Unit) {
|
||||
val blurRadius = appPrefs.appearanceBarsBlurRadius.get()
|
||||
if (blurRadius > 0 && graphicsLayerSize != null && backgroundGraphicsLayer != null) {
|
||||
graphicsLayerSize.value = backgroundGraphicsLayer.size
|
||||
backgroundGraphicsLayer.record {
|
||||
scope()
|
||||
}
|
||||
drawLayer(backgroundGraphicsLayer)
|
||||
} else {
|
||||
scope()
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class AppBarHandler(
|
||||
var graphicsLayer: GraphicsLayer?,
|
||||
var backgroundGraphicsLayer: GraphicsLayer?,
|
||||
val keyboardCoversBar: Boolean = true,
|
||||
listState: LazyListState = LazyListState(0, 0),
|
||||
scrollState: ScrollState = ScrollState(initial = 0)
|
||||
) {
|
||||
@@ -24,6 +76,8 @@ class AppBarHandler(
|
||||
|
||||
val connection = CollapsingAppBarNestedScrollConnection()
|
||||
|
||||
val backgroundGraphicsLayerSize: MutableState<IntSize> = mutableStateOf(IntSize.Zero)
|
||||
|
||||
companion object {
|
||||
var appBarMaxHeightPx: Int = 0
|
||||
}
|
||||
|
||||
-3
@@ -3,7 +3,6 @@ package chat.simplex.common.views.helpers
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.shape.ZeroCornerSize
|
||||
import androidx.compose.foundation.text.*
|
||||
import androidx.compose.material.*
|
||||
@@ -22,13 +21,11 @@ import androidx.compose.ui.text.input.*
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.views.database.PassphraseStrength
|
||||
import chat.simplex.common.views.database.validKey
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun DefaultBasicTextField(
|
||||
modifier: Modifier,
|
||||
|
||||
+2
-1
@@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp
|
||||
@Composable
|
||||
fun DefaultDropdownMenu(
|
||||
showMenu: MutableState<Boolean>,
|
||||
modifier: Modifier = Modifier,
|
||||
offset: DpOffset = DpOffset(0.dp, 0.dp),
|
||||
dropdownMenuItems: (@Composable () -> Unit)?
|
||||
) {
|
||||
@@ -23,7 +24,7 @@ fun DefaultDropdownMenu(
|
||||
DropdownMenu(
|
||||
expanded = showMenu.value,
|
||||
onDismissRequest = { showMenu.value = false },
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.widthIn(min = 250.dp)
|
||||
.background(MaterialTheme.colors.surface)
|
||||
.padding(vertical = 4.dp),
|
||||
|
||||
+184
-59
@@ -3,44 +3,120 @@ package chat.simplex.common.views.helpers
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.item.CenteredRowLayout
|
||||
import chat.simplex.res.MR
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@Composable
|
||||
fun DefaultTopAppBar(
|
||||
fun DefaultAppBar(
|
||||
navigationButton: (@Composable RowScope.() -> Unit)? = null,
|
||||
title: (@Composable () -> Unit)?,
|
||||
title: (@Composable () -> Unit)? = null,
|
||||
fixedTitleText: String? = null,
|
||||
onTitleClick: (() -> Unit)? = null,
|
||||
showSearch: Boolean,
|
||||
onSearchValueChanged: (String) -> Unit,
|
||||
buttons: List<@Composable RowScope.() -> Unit> = emptyList(),
|
||||
onTop: Boolean,
|
||||
showSearch: Boolean = false,
|
||||
searchAlwaysVisible: Boolean = false,
|
||||
onSearchValueChanged: (String) -> Unit = {},
|
||||
buttons: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
// If I just disable clickable modifier when don't need it, it will stop passing clicks to search. Replacing the whole modifier
|
||||
val modifier = if (!showSearch) {
|
||||
Modifier.clickable(enabled = onTitleClick != null, onClick = onTitleClick ?: { })
|
||||
} else Modifier
|
||||
} else Modifier.imePadding()
|
||||
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
if (!showSearch) {
|
||||
title?.invoke()
|
||||
} else {
|
||||
SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = false, onValueChange = onSearchValueChanged)
|
||||
val themeBackgroundMix = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f)
|
||||
val prefAlpha = remember { appPrefs.inAppBarsAlpha.state }
|
||||
val handler = LocalAppBarHandler.current
|
||||
val connection = LocalAppBarHandler.current?.connection
|
||||
val titleText = remember(handler?.title?.value, fixedTitleText) {
|
||||
if (fixedTitleText != null) {
|
||||
mutableStateOf(fixedTitleText)
|
||||
} else {
|
||||
handler?.title ?: mutableStateOf("")
|
||||
}
|
||||
}
|
||||
val keyboardInset = WindowInsets.ime
|
||||
Box(modifier) {
|
||||
val density = LocalDensity.current
|
||||
val blurRadius = remember { appPrefs.appearanceBarsBlurRadius.state }
|
||||
Box(Modifier
|
||||
.matchParentSize()
|
||||
.blurredBackgroundModifier(keyboardInset, handler, blurRadius, prefAlpha, handler?.keyboardCoversBar == true, onTop, density)
|
||||
.drawWithCache {
|
||||
// store it as a variable, don't put it inside if without holding it here. Compiler don't see it changes otherwise
|
||||
val alpha = prefAlpha.value
|
||||
val backgroundColor = if (title != null || fixedTitleText != null || connection == null || !onTop) {
|
||||
themeBackgroundMix.copy(alpha)
|
||||
} else {
|
||||
themeBackgroundMix.copy(topTitleAlpha(false, connection))
|
||||
}
|
||||
onDrawBehind {
|
||||
drawRect(backgroundColor)
|
||||
}
|
||||
}
|
||||
},
|
||||
backgroundColor = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f),
|
||||
navigationIcon = navigationButton,
|
||||
buttons = if (!showSearch) buttons else emptyList(),
|
||||
centered = !showSearch,
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.then(if (!onTop) Modifier.navigationBarsPadding() else Modifier)
|
||||
.heightIn(min = AppBarHeight * fontSizeSqrtMultiplier)
|
||||
) {
|
||||
AppBar(
|
||||
title = {
|
||||
if (showSearch) {
|
||||
SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = searchAlwaysVisible, reducedCloseButtonPadding = 12.dp, onValueChange = onSearchValueChanged)
|
||||
} else if (title != null) {
|
||||
title()
|
||||
} else if (titleText.value.isNotEmpty() && connection != null) {
|
||||
Row(
|
||||
Modifier
|
||||
.graphicsLayer {
|
||||
alpha = if (fixedTitleText != null) 1f else topTitleAlpha(true, connection)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
titleText.value,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
navigationIcon = navigationButton,
|
||||
buttons = if (!showSearch) buttons else {{}},
|
||||
centered = !showSearch && (title != null || !onTop),
|
||||
onTop = onTop,
|
||||
)
|
||||
AppBarDivider(onTop, title != null || fixedTitleText != null, connection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun CallAppBar(
|
||||
title: @Composable () -> Unit,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
AppBar(
|
||||
title,
|
||||
navigationIcon = { NavigationButtonBack(tintColor = Color(0xFFFFFFD8), onButtonClicked = onBack) },
|
||||
centered = false,
|
||||
onTop = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -83,58 +159,107 @@ fun NavigationButtonMenu(onButtonClicked: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopAppBar(
|
||||
private fun BoxScope.AppBarDivider(onTop: Boolean, fixedAlpha: Boolean, connection: CollapsingAppBarNestedScrollConnection?) {
|
||||
if (connection != null) {
|
||||
Divider(
|
||||
Modifier
|
||||
.align(if (onTop) Alignment.BottomStart else Alignment.TopStart)
|
||||
.graphicsLayer {
|
||||
alpha = if (!onTop || fixedAlpha) 1f else topTitleAlpha(false, connection, 1f)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Divider(Modifier.align(if (onTop) Alignment.BottomStart else Alignment.TopStart))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppBar(
|
||||
title: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
navigationIcon: @Composable (RowScope.() -> Unit)? = null,
|
||||
buttons: List<@Composable RowScope.() -> Unit> = emptyList(),
|
||||
backgroundColor: Color = MaterialTheme.colors.primarySurface,
|
||||
buttons: @Composable RowScope.() -> Unit = {},
|
||||
centered: Boolean,
|
||||
onTop: Boolean,
|
||||
) {
|
||||
Box(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.height(AppBarHeight * fontSizeSqrtMultiplier)
|
||||
.background(backgroundColor)
|
||||
.padding(horizontal = 4.dp),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
val adjustedModifier = modifier
|
||||
.then(if (onTop) Modifier.statusBarsPadding() else Modifier)
|
||||
.height(AppBarHeight * fontSizeSqrtMultiplier)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AppBarHorizontalPadding)
|
||||
if (centered) {
|
||||
AppBarCenterAligned(adjustedModifier, title, navigationIcon, buttons)
|
||||
} else {
|
||||
AppBarStartAligned(adjustedModifier, title, navigationIcon, buttons)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppBarStartAligned(
|
||||
modifier: Modifier,
|
||||
title: @Composable () -> Unit,
|
||||
navigationIcon: @Composable (RowScope.() -> Unit)? = null,
|
||||
buttons: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (navigationIcon != null) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.width(TitleInsetWithIcon - AppBarHorizontalPadding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
content = navigationIcon
|
||||
)
|
||||
navigationIcon()
|
||||
Spacer(Modifier.width(AppBarHorizontalPadding))
|
||||
} else {
|
||||
Spacer(Modifier.width(DEFAULT_PADDING))
|
||||
}
|
||||
Row(Modifier
|
||||
.weight(1f)
|
||||
.padding(end = DEFAULT_PADDING_HALF)
|
||||
) {
|
||||
title()
|
||||
}
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
buttons.forEach { it() }
|
||||
}
|
||||
val startPadding = if (navigationIcon != null) TitleInsetWithIcon else TitleInsetWithoutIcon
|
||||
val endPadding = (buttons.size * 50f).dp
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = if (centered) kotlin.math.max(startPadding.value, endPadding.value).dp else startPadding,
|
||||
end = if (centered) kotlin.math.max(startPadding.value, endPadding.value).dp else endPadding
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
title()
|
||||
buttons()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppBarCenterAligned(
|
||||
modifier: Modifier,
|
||||
title: @Composable () -> Unit,
|
||||
navigationIcon: @Composable (RowScope.() -> Unit)? = null,
|
||||
buttons: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
CenteredRowLayout(modifier) {
|
||||
if (navigationIcon != null) {
|
||||
Row(
|
||||
Modifier.padding(end = AppBarHorizontalPadding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
content = navigationIcon
|
||||
)
|
||||
} else {
|
||||
Spacer(Modifier)
|
||||
}
|
||||
Row(
|
||||
Modifier.padding(end = DEFAULT_PADDING_HALF)
|
||||
) {
|
||||
title()
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
buttons()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun topTitleAlpha(text: Boolean, connection: CollapsingAppBarNestedScrollConnection, alpha: Float = appPrefs.inAppBarsAlpha.get()) =
|
||||
if (connection.appBarOffset.absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 0f
|
||||
else ((-connection.appBarOffset * 1.5f) / (AppBarHandler.appBarMaxHeightPx)).coerceIn(0f, if (text) 1f else alpha)
|
||||
|
||||
val AppBarHeight = 56.dp
|
||||
val AppBarHorizontalPadding = 4.dp
|
||||
val BottomAppBarHeight = 60.dp
|
||||
private val TitleInsetWithoutIcon = DEFAULT_PADDING - AppBarHorizontalPadding
|
||||
val TitleInsetWithIcon = 72.dp
|
||||
val AppBarHorizontalPadding = 2.dp
|
||||
|
||||
+2
-2
@@ -6,7 +6,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -107,6 +106,7 @@ fun <T> ExposedDropDownSettingWithIcon(
|
||||
expanded.value = !expanded.value && enabled.value
|
||||
}
|
||||
) {
|
||||
val ripple = remember { ripple(bounded = false, radius = boxSize / 2, color = background.lighter(0.1f)) }
|
||||
Box(
|
||||
Modifier
|
||||
.background(background, CircleShape)
|
||||
@@ -115,7 +115,7 @@ fun <T> ExposedDropDownSettingWithIcon(
|
||||
onClick = {},
|
||||
role = Role.Button,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false, radius = boxSize / 2, color = background.lighter(0.1f)),
|
||||
indication = ripple,
|
||||
enabled = enabled.value
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
|
||||
+36
-24
@@ -6,12 +6,14 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chatlist.StatusBarBackground
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.math.min
|
||||
@@ -21,24 +23,40 @@ import kotlin.math.sqrt
|
||||
fun ModalView(
|
||||
close: () -> Unit,
|
||||
showClose: Boolean = true,
|
||||
showAppBar: Boolean = true,
|
||||
enableClose: Boolean = true,
|
||||
background: Color = MaterialTheme.colors.background,
|
||||
background: Color = Color.Unspecified,
|
||||
modifier: Modifier = Modifier,
|
||||
closeOnTop: Boolean = true,
|
||||
showSearch: Boolean = false,
|
||||
searchAlwaysVisible: Boolean = false,
|
||||
onSearchValueChanged: (String) -> Unit = {},
|
||||
endButtons: @Composable RowScope.() -> Unit = {},
|
||||
content: @Composable () -> Unit,
|
||||
content: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
if (showClose) {
|
||||
if (showClose && showAppBar) {
|
||||
BackHandler(enabled = enableClose, onBack = close)
|
||||
}
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) {
|
||||
Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) {
|
||||
if (closeOnTop) {
|
||||
CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons)
|
||||
}
|
||||
Box(if (background != Color.Unspecified) Modifier.background(background) else Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) {
|
||||
Box(modifier = modifier) {
|
||||
content()
|
||||
}
|
||||
if (showAppBar) {
|
||||
if (oneHandUI.value) {
|
||||
StatusBarBackground()
|
||||
}
|
||||
Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) {
|
||||
DefaultAppBar(
|
||||
navigationButton = if (showClose) {{ NavigationButtonBack(onButtonClicked = if (enableClose) close else null) }} else null,
|
||||
onTop = !oneHandUI.value,
|
||||
showSearch = showSearch,
|
||||
searchAlwaysVisible = searchAlwaysVisible,
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
buttons = endButtons
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +65,7 @@ enum class ModalPlacement {
|
||||
START, CENTER, END, FULLSCREEN
|
||||
}
|
||||
|
||||
class ModalData() {
|
||||
class ModalData(val keyboardCoversBar: Boolean = true) {
|
||||
private val state = mutableMapOf<String, MutableState<Any?>>()
|
||||
fun <T> stateGetOrPut (key: String, default: () -> T): MutableState<T> =
|
||||
state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState<T>
|
||||
@@ -55,7 +73,7 @@ class ModalData() {
|
||||
fun <T> stateGetOrPutNullable (key: String, default: () -> T?): MutableState<T?> =
|
||||
state.getOrPut(key) { mutableStateOf(default() as Any?) } as MutableState<T?>
|
||||
|
||||
val appBarHandler = AppBarHandler()
|
||||
val appBarHandler = AppBarHandler(null, null, keyboardCoversBar = keyboardCoversBar)
|
||||
}
|
||||
|
||||
class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
@@ -69,23 +87,21 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
|
||||
private var onTimePasscodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
|
||||
|
||||
fun showModal(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
|
||||
val data = ModalData()
|
||||
fun showModal(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
|
||||
showCustomModal { close ->
|
||||
ModalView(close, showClose = showClose, closeOnTop = closeOnTop, endButtons = endButtons, content = { data.content() })
|
||||
ModalView(close, showClose = showClose, endButtons = endButtons, content = { content() })
|
||||
}
|
||||
}
|
||||
|
||||
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||
val data = ModalData()
|
||||
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||
showCustomModal { close ->
|
||||
ModalView(close, showClose = showClose, endButtons = endButtons, closeOnTop = closeOnTop, content = { data.content(close) })
|
||||
ModalView(close, showClose = showClose, endButtons = endButtons, content = { content(close) })
|
||||
}
|
||||
}
|
||||
|
||||
fun showCustomModal(animated: Boolean = true, modal: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||
fun showCustomModal(animated: Boolean = true, keyboardCoversBar: Boolean = true, modal: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||
Log.d(TAG, "ModalManager.showCustomModal")
|
||||
val data = ModalData()
|
||||
val data = ModalData(keyboardCoversBar = keyboardCoversBar)
|
||||
// Means, animation is in progress or not started yet. Do not wait until animation finishes, just remove all from screen.
|
||||
// This is useful when invoking close() and ShowCustomModal one after another without delay. Otherwise, screen will hold prev view
|
||||
if (toRemove.isNotEmpty()) {
|
||||
@@ -146,9 +162,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
// Without animation
|
||||
if (modalCount.value > 0 && modalViews.lastOrNull()?.first == false) {
|
||||
modalViews.lastOrNull()?.let {
|
||||
CompositionLocalProvider(
|
||||
LocalAppBarHandler provides it.second.appBarHandler
|
||||
) {
|
||||
CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.second.appBarHandler)) {
|
||||
it.third(it.second, ::closeModal)
|
||||
}
|
||||
}
|
||||
@@ -164,9 +178,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
}
|
||||
) {
|
||||
modalViews.getOrNull(it - 1)?.let {
|
||||
CompositionLocalProvider(
|
||||
LocalAppBarHandler provides it.second.appBarHandler
|
||||
) {
|
||||
CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.second.appBarHandler)) {
|
||||
it.third(it.second, ::closeModal)
|
||||
}
|
||||
}
|
||||
|
||||
+12
-13
@@ -2,7 +2,7 @@ package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.ZeroCornerSize
|
||||
import androidx.compose.foundation.text.*
|
||||
import androidx.compose.material.*
|
||||
@@ -18,12 +18,9 @@ import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.*
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -38,6 +35,7 @@ fun SearchTextField(
|
||||
placeholder: String = stringResource(MR.strings.search_verb),
|
||||
enabled: Boolean = true,
|
||||
trailingContent: @Composable (() -> Unit)? = null,
|
||||
reducedCloseButtonPadding: Dp = 0.dp,
|
||||
onValueChange: (String) -> Unit
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
@@ -81,15 +79,20 @@ fun SearchTextField(
|
||||
)
|
||||
val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
|
||||
// sizing is done differently on Android and desktop in order to have the same height of search and compose view on desktop
|
||||
// see PlatformTextField.desktop + SendMsgView
|
||||
val padding = if (appPlatform.isAndroid) PaddingValues() else PaddingValues(top = 3.dp, bottom = 4.dp)
|
||||
BasicTextField(
|
||||
value = searchText.value,
|
||||
modifier = modifier
|
||||
.background(colors.backgroundColor(enabled).value, shape)
|
||||
.indicatorLine(enabled, false, interactionSource, colors)
|
||||
.focusRequester(focusRequester)
|
||||
.padding(padding)
|
||||
.defaultMinSize(
|
||||
minWidth = TextFieldDefaults.MinWidth,
|
||||
minHeight = TextFieldDefaults.MinHeight
|
||||
minHeight = if (appPlatform.isAndroid) TextFieldDefaults.MinHeight else 0.dp
|
||||
),
|
||||
onValueChange = {
|
||||
searchText.value = it
|
||||
@@ -100,18 +103,14 @@ fun SearchTextField(
|
||||
visualTransformation = VisualTransformation.None,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
singleLine = true,
|
||||
textStyle = TextStyle(
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 15.sp
|
||||
),
|
||||
textStyle = textStyle,
|
||||
interactionSource = interactionSource,
|
||||
decorationBox = @Composable { innerTextField ->
|
||||
TextFieldDefaults.TextFieldDecorationBox(
|
||||
value = searchText.value.text,
|
||||
innerTextField = innerTextField,
|
||||
placeholder = {
|
||||
Text(placeholder, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(placeholder, style = textStyle.copy(color = MaterialTheme.colors.secondary), maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
},
|
||||
trailingIcon = if (searchText.value.text.isNotEmpty()) {{
|
||||
IconButton({
|
||||
@@ -121,7 +120,7 @@ fun SearchTextField(
|
||||
}
|
||||
searchText.value = TextFieldValue("");
|
||||
onValueChange("")
|
||||
}) {
|
||||
}, Modifier.offset(x = reducedCloseButtonPadding)) {
|
||||
Icon(painterResource(MR.images.ic_close), stringResource(MR.strings.icon_descr_close_button), tint = MaterialTheme.colors.primary,)
|
||||
}
|
||||
}} else trailingContent,
|
||||
|
||||
+1
-1
@@ -57,7 +57,6 @@ fun TextEditor(
|
||||
) {
|
||||
val textFieldModifier = modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsWithImePadding()
|
||||
.onFocusChanged { focused = it.isFocused }
|
||||
.padding(10.dp)
|
||||
|
||||
@@ -87,6 +86,7 @@ fun TextEditor(
|
||||
enabled = true,
|
||||
isError = false,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
+2
-8
@@ -32,10 +32,7 @@ fun ModalData.UserWallpaperEditor(
|
||||
globalThemeUsed: MutableState<Boolean>,
|
||||
save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } }
|
||||
var showMore by remember { stateGetOrPut("showMore") { false } }
|
||||
val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } }
|
||||
@@ -231,10 +228,7 @@ fun ModalData.ChatWallpaperEditor(
|
||||
globalThemeUsed: MutableState<Boolean>,
|
||||
save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } }
|
||||
var showMore by remember { stateGetOrPut("showMore") { false } }
|
||||
val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } }
|
||||
|
||||
+1
-3
@@ -149,9 +149,7 @@ private fun MigrateFromDeviceLayout(
|
||||
) {
|
||||
val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) }
|
||||
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(), maxIntrinsicSize = true
|
||||
) {
|
||||
ColumnWithScrollBar(maxIntrinsicSize = true) {
|
||||
AppBarTitle(stringResource(MR.strings.migrate_from_device_title))
|
||||
SectionByState(migrationState, tempDatabaseFile.value, chatReceiver)
|
||||
SectionBottomSpacer()
|
||||
|
||||
+1
-3
@@ -162,9 +162,7 @@ private fun ModalData.MigrateToDeviceLayout(
|
||||
close: () -> Unit,
|
||||
) {
|
||||
val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) }
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(), maxIntrinsicSize = true
|
||||
) {
|
||||
ColumnWithScrollBar(maxIntrinsicSize = true) {
|
||||
AppBarTitle(stringResource(MR.strings.migrate_to_device_title))
|
||||
SectionByState(migrationState, tempDatabaseFile.value, chatReceiver, close)
|
||||
SectionBottomSpacer()
|
||||
|
||||
+1
-3
@@ -15,9 +15,7 @@ import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun AddContactLearnMore(close: () -> Unit) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.padding(horizontal = DEFAULT_PADDING),
|
||||
) {
|
||||
ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||
AppBarTitle(stringResource(MR.strings.one_time_link), withPadding = false)
|
||||
ReadableText(MR.strings.scan_qr_to_connect_to_contact)
|
||||
ReadableText(MR.strings.if_you_cant_meet_in_person)
|
||||
|
||||
+6
-10
@@ -84,10 +84,9 @@ fun AddGroupLayout(
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val incognito = remember { mutableStateOf(incognitoPref.get()) }
|
||||
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
ModalBottomSheetLayout(
|
||||
scrimColor = Color.Black.copy(alpha = 0.12F),
|
||||
modifier = Modifier.navigationBarsWithImePadding(),
|
||||
modifier = Modifier.imePadding(),
|
||||
sheetContent = {
|
||||
GetImageBottomSheet(
|
||||
chosenImage,
|
||||
@@ -100,11 +99,7 @@ fun AddGroupLayout(
|
||||
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
|
||||
) {
|
||||
ModalView(close = close) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.create_secret_group_title), hostDevice(rhId))
|
||||
Box(
|
||||
Modifier
|
||||
@@ -122,7 +117,7 @@ fun AddGroupLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Row(Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(
|
||||
stringResource(MR.strings.group_display_name_field),
|
||||
fontSize = 16.sp
|
||||
@@ -134,7 +129,9 @@ fun AddGroupLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
ProfileNameField(displayName, "", { isValidDisplayName(it.trim()) }, focusRequester)
|
||||
Box(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||
ProfileNameField(displayName, "", { isValidDisplayName(it.trim()) }, focusRequester)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
SettingsActionItem(
|
||||
@@ -170,7 +167,6 @@ fun AddGroupLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun canCreateProfile(displayName: String): Boolean = displayName.trim().isNotEmpty() && isValidDisplayName(displayName.trim())
|
||||
|
||||
+2
-4
@@ -89,7 +89,7 @@ private fun ContactConnectionInfoLayout(
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_theater_comedy_filled),
|
||||
text = null,
|
||||
click = { ModalManager.start.showModal { IncognitoView() } },
|
||||
click = { ModalManager.end.showModal { IncognitoView() } },
|
||||
iconColor = Indigo,
|
||||
extraPadding = false
|
||||
) {
|
||||
@@ -105,9 +105,7 @@ private fun ContactConnectionInfoLayout(
|
||||
}
|
||||
}
|
||||
|
||||
ColumnWithScrollBar(
|
||||
Modifier,
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(
|
||||
stringResource(
|
||||
if (contactConnection.initiated) MR.strings.you_invited_a_contact
|
||||
|
||||
+337
-246
@@ -1,9 +1,7 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import SectionDivider
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import TextIconSpaced
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
@@ -14,8 +12,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
@@ -32,56 +29,43 @@ import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chatlist.ScrollDirection
|
||||
import chat.simplex.common.views.chat.topPaddingToContent
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.contacts.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val keyboardState by getKeyboardState()
|
||||
val showToolbarInOneHandUI = remember { derivedStateOf { keyboardState == KeyboardState.Closed && oneHandUI.value } }
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
if (showToolbarInOneHandUI.value) {
|
||||
Column {
|
||||
Divider()
|
||||
CloseSheetBar(
|
||||
close = close,
|
||||
showClose = true,
|
||||
endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) },
|
||||
arrangement = Arrangement.Bottom,
|
||||
closeBarTitle = generalGetString(MR.strings.new_message),
|
||||
barPaddingValues = PaddingValues(horizontal = 0.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Box {
|
||||
val closeAll = { ModalManager.start.closeModals() }
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
NewChatSheetLayout(
|
||||
addContact = {
|
||||
ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll) }
|
||||
},
|
||||
scanPaste = {
|
||||
ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = closeAll) }
|
||||
},
|
||||
createGroup = {
|
||||
ModalManager.start.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) }
|
||||
},
|
||||
rh = rh,
|
||||
close = close
|
||||
)
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(it)
|
||||
) {
|
||||
val closeAll = { ModalManager.start.closeModals() }
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
NewChatSheetLayout(
|
||||
addContact = {
|
||||
ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll ) }
|
||||
},
|
||||
scanPaste = {
|
||||
ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = closeAll) }
|
||||
},
|
||||
createGroup = {
|
||||
ModalManager.start.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) }
|
||||
},
|
||||
rh = rh,
|
||||
close = close
|
||||
if (oneHandUI.value) {
|
||||
Column(Modifier.align(Alignment.BottomCenter)) {
|
||||
DefaultAppBar(
|
||||
navigationButton = { NavigationButtonBack(onButtonClicked = close) },
|
||||
fixedTitleText = generalGetString(MR.strings.new_message),
|
||||
onTop = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -187,168 +171,258 @@ private fun ModalData.NewChatSheetLayout(
|
||||
derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) }
|
||||
}
|
||||
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
listState,
|
||||
reverseLayout = oneHandUI.value
|
||||
) {
|
||||
if (!oneHandUI.value) {
|
||||
item {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.new_message),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = bottomPadding
|
||||
val actionButtonsOriginal = listOf(
|
||||
Triple(
|
||||
painterResource(MR.images.ic_add_link),
|
||||
stringResource(MR.strings.add_contact_tab),
|
||||
addContact,
|
||||
),
|
||||
Triple(
|
||||
painterResource(MR.images.ic_qr_code),
|
||||
if (appPlatform.isAndroid) stringResource(MR.strings.scan_paste_link) else stringResource(MR.strings.paste_link),
|
||||
scanPaste,
|
||||
),
|
||||
Triple(
|
||||
painterResource(MR.images.ic_group),
|
||||
stringResource(MR.strings.create_group_button),
|
||||
createGroup,
|
||||
)
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun DeletedChatsItem(actionButtons: List<Triple<Painter, String, () -> Unit>>) {
|
||||
if (searchText.value.text.isEmpty()) {
|
||||
Spacer(Modifier.padding(bottom = 27.dp))
|
||||
}
|
||||
|
||||
if (searchText.value.text.isEmpty()) {
|
||||
Row {
|
||||
SectionView {
|
||||
actionButtons.map {
|
||||
NewChatButton(
|
||||
icon = it.first,
|
||||
text = it.second,
|
||||
click = it.third,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (deletedChats.isNotEmpty()) {
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
SectionView {
|
||||
SectionItemView(
|
||||
click = {
|
||||
ModalManager.start.showCustomModal { closeDeletedChats ->
|
||||
ModalView(
|
||||
close = closeDeletedChats,
|
||||
showAppBar = !oneHandUI.value,
|
||||
) {
|
||||
if (oneHandUI.value) {
|
||||
BackHandler(onBack = closeDeletedChats)
|
||||
}
|
||||
DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = {
|
||||
ModalManager.start.closeModals()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_inventory_2),
|
||||
contentDescription = stringResource(MR.strings.deleted_chats),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
)
|
||||
TextIconSpaced(false)
|
||||
Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoFilteredContactsItem() {
|
||||
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
|
||||
Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
generalGetString(MR.strings.no_filtered_contacts),
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
stickyHeader {
|
||||
Column(
|
||||
Modifier
|
||||
.offset {
|
||||
val y = if (searchText.value.text.isEmpty()) {
|
||||
val offsetMultiplier = if (oneHandUI.value) 1 else -1
|
||||
}
|
||||
|
||||
if (
|
||||
(oneHandUI.value && scrollDirection == ScrollDirection.Up) ||
|
||||
(appPlatform.isAndroid && keyboardState == KeyboardState.Opened)
|
||||
) {
|
||||
0
|
||||
} else if (oneHandUI.value && listState.firstVisibleItemIndex == 0) {
|
||||
listState.firstVisibleItemScrollOffset
|
||||
} else if (!oneHandUI.value && listState.firstVisibleItemIndex == 0) {
|
||||
0
|
||||
} else if (!oneHandUI.value && listState.firstVisibleItemIndex == 1) {
|
||||
-listState.firstVisibleItemScrollOffset
|
||||
@Composable
|
||||
fun OneHandLazyColumn() {
|
||||
val blankSpaceSize = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier
|
||||
LazyColumnWithScrollBar(
|
||||
state = listState,
|
||||
reverseLayout = oneHandUI.value
|
||||
) {
|
||||
item { Spacer(Modifier.height(blankSpaceSize)) }
|
||||
stickyHeader {
|
||||
val scrolledSomething by remember { derivedStateOf { listState.firstVisibleItemScrollOffset > 0 || listState.firstVisibleItemIndex > 0 } }
|
||||
Column(
|
||||
Modifier
|
||||
.zIndex(1f)
|
||||
.offset {
|
||||
val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened)) {
|
||||
if (listState.firstVisibleItemIndex == 0) -minOf(listState.firstVisibleItemScrollOffset, blankSpaceSize.roundToPx())
|
||||
else -blankSpaceSize.roundToPx()
|
||||
} else {
|
||||
offsetMultiplier * 1000
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
IntOffset(0, y)
|
||||
}
|
||||
.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
Divider()
|
||||
ContactsSearchBar(
|
||||
listState = listState,
|
||||
searchText = searchText,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
close = close,
|
||||
)
|
||||
if (!oneHandUI.value) {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
if (searchText.value.text.isEmpty()) {
|
||||
Spacer(Modifier.padding(bottom = 27.dp))
|
||||
}
|
||||
|
||||
val actionButtonsOriginal = listOf(
|
||||
Triple(
|
||||
painterResource(MR.images.ic_add_link),
|
||||
stringResource(MR.strings.add_contact_tab),
|
||||
addContact,
|
||||
),
|
||||
Triple(
|
||||
painterResource(MR.images.ic_qr_code),
|
||||
if (appPlatform.isAndroid) stringResource(MR.strings.scan_paste_link) else stringResource(MR.strings.paste_link),
|
||||
scanPaste,
|
||||
),
|
||||
Triple(
|
||||
painterResource(MR.images.ic_group),
|
||||
stringResource(MR.strings.create_group_button),
|
||||
createGroup,
|
||||
)
|
||||
)
|
||||
|
||||
val actionButtons by remember(oneHandUI.value) {
|
||||
derivedStateOf {
|
||||
if (oneHandUI.value) actionButtonsOriginal.asReversed() else actionButtonsOriginal
|
||||
}
|
||||
}
|
||||
|
||||
if (searchText.value.text.isEmpty()) {
|
||||
Row {
|
||||
SectionView {
|
||||
actionButtons.map {
|
||||
NewChatButton(
|
||||
icon = it.first,
|
||||
text = it.second,
|
||||
click = it.third,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (deletedChats.isNotEmpty()) {
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
SectionView {
|
||||
SectionItemView(
|
||||
click = {
|
||||
ModalManager.start.showCustomModal { closeDeletedChats ->
|
||||
ModalView(
|
||||
close = closeDeletedChats,
|
||||
closeOnTop = !oneHandUI.value,
|
||||
) {
|
||||
DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = {
|
||||
ModalManager.start.closeModals()
|
||||
})
|
||||
}
|
||||
when (listState.firstVisibleItemIndex) {
|
||||
0 -> 0
|
||||
1 -> listState.firstVisibleItemScrollOffset
|
||||
else -> 1000
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_inventory_2),
|
||||
contentDescription = stringResource(MR.strings.deleted_chats),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
)
|
||||
TextIconSpaced(false)
|
||||
Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground)
|
||||
IntOffset(0, y)
|
||||
}
|
||||
// show background when something is scrolled because otherwise the bar is transparent.
|
||||
// not using background always because of gradient in SimpleX theme
|
||||
.background(
|
||||
if (scrolledSomething && (keyboardState == KeyboardState.Opened || searchText.value.text.isNotEmpty())) {
|
||||
MaterialTheme.colors.background
|
||||
} else {
|
||||
Color.Unspecified
|
||||
}
|
||||
)
|
||||
) {
|
||||
Divider()
|
||||
Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight * fontSizeSqrtMultiplier))) {
|
||||
ContactsSearchBar(
|
||||
listState = listState,
|
||||
searchText = searchText,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
close = close,
|
||||
)
|
||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) {
|
||||
if (!oneHandUI.value) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
|
||||
} else {
|
||||
item {
|
||||
DeletedChatsItem(actionButtonsOriginal.asReversed())
|
||||
}
|
||||
item {
|
||||
if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) {
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
|
||||
SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
|
||||
Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
generalGetString(MR.strings.no_filtered_contacts),
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
item {
|
||||
NoFilteredContactsItem()
|
||||
}
|
||||
itemsIndexed(filteredContactChats) { index, chat ->
|
||||
val nextChatSelected = remember(chat.id, filteredContactChats) {
|
||||
derivedStateOf {
|
||||
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
}
|
||||
}
|
||||
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true)
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(filteredContactChats) { index, chat ->
|
||||
val nextChatSelected = remember(chat.id, filteredContactChats) {
|
||||
derivedStateOf {
|
||||
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
if (appPlatform.isAndroid) {
|
||||
item {
|
||||
Spacer(Modifier.windowInsetsTopHeight(WindowInsets.statusBars))
|
||||
}
|
||||
}
|
||||
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NonOneHandLazyColumn() {
|
||||
val blankSpaceSize = topPaddingToContent()
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.imePadding(),
|
||||
state = listState,
|
||||
reverseLayout = false
|
||||
) {
|
||||
item {
|
||||
Box(Modifier.padding(top = blankSpaceSize)) {
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.new_message),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = DEFAULT_PADDING
|
||||
)
|
||||
}
|
||||
}
|
||||
stickyHeader {
|
||||
val scrolledSomething by remember { derivedStateOf { listState.firstVisibleItemScrollOffset > 0 || listState.firstVisibleItemIndex > 0 } }
|
||||
Column(
|
||||
Modifier
|
||||
.zIndex(1f)
|
||||
.offset {
|
||||
val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened)) {
|
||||
if (listState.firstVisibleItemIndex == 0) (listState.firstVisibleItemScrollOffset - (listState.layoutInfo.visibleItemsInfo[0].size - blankSpaceSize.roundToPx())).coerceAtLeast(0)
|
||||
else blankSpaceSize.roundToPx()
|
||||
} else {
|
||||
when (listState.firstVisibleItemIndex) {
|
||||
0 -> 0
|
||||
1 -> -listState.firstVisibleItemScrollOffset
|
||||
else -> -1000
|
||||
}
|
||||
}
|
||||
IntOffset(0, y)
|
||||
}
|
||||
// show background when something is scrolled because otherwise the bar is transparent.
|
||||
// not using background always because of gradient in SimpleX theme
|
||||
.background(
|
||||
if (scrolledSomething && (keyboardState == KeyboardState.Opened || searchText.value.text.isNotEmpty())) {
|
||||
MaterialTheme.colors.background
|
||||
} else {
|
||||
Color.Unspecified
|
||||
}
|
||||
)
|
||||
) {
|
||||
Divider()
|
||||
ContactsSearchBar(
|
||||
listState = listState,
|
||||
searchText = searchText,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
close = close,
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
item {
|
||||
DeletedChatsItem(actionButtonsOriginal)
|
||||
}
|
||||
item {
|
||||
if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
|
||||
}
|
||||
}
|
||||
item {
|
||||
NoFilteredContactsItem()
|
||||
}
|
||||
itemsIndexed(filteredContactChats) { index, chat ->
|
||||
val nextChatSelected = remember(chat.id, filteredContactChats) {
|
||||
derivedStateOf {
|
||||
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
}
|
||||
}
|
||||
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true)
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
item {
|
||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box {
|
||||
if (oneHandUI.value) {
|
||||
OneHandLazyColumn()
|
||||
StatusBarBackground()
|
||||
} else {
|
||||
NonOneHandLazyColumn()
|
||||
NavigationBarBackground(oneHandUI.value, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -554,26 +628,7 @@ private fun contactTypesSearchTargets(baseContactTypes: List<ContactType>, searc
|
||||
@Composable
|
||||
private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Unit, close: () -> Unit) {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val keyboardState by getKeyboardState()
|
||||
val showToolbarInOneHandUI = remember { derivedStateOf { keyboardState == KeyboardState.Closed && oneHandUI.value } }
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
if (showToolbarInOneHandUI.value) {
|
||||
Column {
|
||||
Divider()
|
||||
CloseSheetBar(
|
||||
close = closeDeletedChats,
|
||||
showClose = true,
|
||||
endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) },
|
||||
arrangement = Arrangement.Bottom,
|
||||
closeBarTitle = generalGetString(MR.strings.deleted_chats),
|
||||
barPaddingValues = PaddingValues(horizontal = 0.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { contentPadding ->
|
||||
Box {
|
||||
val listState = remember { appBarHandler.listState }
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
||||
@@ -590,57 +645,93 @@ private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats
|
||||
contactChats = allChats
|
||||
)
|
||||
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
contentPadding = contentPadding,
|
||||
reverseLayout = oneHandUI.value,
|
||||
) {
|
||||
item {
|
||||
if (!oneHandUI.value) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.deleted_chats),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = bottomPadding
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
if (!oneHandUI.value) {
|
||||
Divider()
|
||||
}
|
||||
ContactsSearchBar(
|
||||
listState = listState,
|
||||
searchText = searchText,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
close = close,
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
|
||||
item {
|
||||
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
|
||||
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
generalGetString(MR.strings.no_filtered_contacts),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
Box {
|
||||
val topPaddingToContent = topPaddingToContent()
|
||||
LazyColumnWithScrollBar(
|
||||
if (!oneHandUI.value) Modifier.imePadding() else Modifier,
|
||||
contentPadding = PaddingValues(
|
||||
top = if (!oneHandUI.value) topPaddingToContent else 0.dp,
|
||||
bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp
|
||||
),
|
||||
reverseLayout = oneHandUI.value,
|
||||
) {
|
||||
item {
|
||||
if (!oneHandUI.value) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.deleted_chats),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = bottomPadding
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
if (!oneHandUI.value) {
|
||||
Divider()
|
||||
ContactsSearchBar(
|
||||
listState = listState,
|
||||
searchText = searchText,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
close = close,
|
||||
)
|
||||
} else {
|
||||
Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) {
|
||||
ContactsSearchBar(
|
||||
listState = listState,
|
||||
searchText = searchText,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
close = close,
|
||||
)
|
||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
|
||||
itemsIndexed(filteredContactChats) { index, chat ->
|
||||
val nextChatSelected = remember(chat.id, filteredContactChats) {
|
||||
derivedStateOf {
|
||||
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
item {
|
||||
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
|
||||
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
generalGetString(MR.strings.no_filtered_contacts),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = false)
|
||||
|
||||
itemsIndexed(filteredContactChats) { index, chat ->
|
||||
val nextChatSelected = remember(chat.id, filteredContactChats) {
|
||||
derivedStateOf {
|
||||
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
}
|
||||
}
|
||||
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = false)
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
item {
|
||||
Spacer(if (oneHandUI.value) Modifier.windowInsetsTopHeight(WindowInsets.statusBars) else Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oneHandUI.value) {
|
||||
StatusBarBackground()
|
||||
} else {
|
||||
NavigationBarBackground(oneHandUI.value, true)
|
||||
}
|
||||
}
|
||||
if (oneHandUI.value) {
|
||||
Column(Modifier.align(Alignment.BottomCenter)) {
|
||||
DefaultAppBar(
|
||||
navigationButton = { NavigationButtonBack(onButtonClicked = closeDeletedChats) },
|
||||
fixedTitleText = generalGetString(MR.strings.deleted_chats),
|
||||
onTop = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-5
@@ -29,10 +29,12 @@ import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.topPaddingToContent
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.res.MR
|
||||
@@ -398,8 +400,12 @@ fun ActiveProfilePicker(
|
||||
.fillMaxSize()
|
||||
.alpha(if (progressByTimeout) 0.6f else 1f)
|
||||
) {
|
||||
LazyColumnWithScrollBar(userScrollEnabled = !switchingProfile.value) {
|
||||
LazyColumnWithScrollBar(Modifier.padding(top = topPaddingToContent()), userScrollEnabled = !switchingProfile.value) {
|
||||
item {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
if (oneHandUI.value) {
|
||||
Spacer(Modifier.padding(top = DEFAULT_PADDING + 5.dp))
|
||||
}
|
||||
AppBarTitle(stringResource(MR.strings.select_chat_profile), hostDevice(rhId), bottomPadding = DEFAULT_PADDING)
|
||||
}
|
||||
val activeProfile = filteredProfiles.firstOrNull { it.activeUser }
|
||||
@@ -434,6 +440,9 @@ fun ActiveProfilePicker(
|
||||
ProfilePickerUserOption(p)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(Modifier.imePadding().padding(bottom = DEFAULT_BOTTOM_PADDING))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (progressByTimeout) {
|
||||
@@ -472,13 +481,13 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection
|
||||
end = 16.dp
|
||||
),
|
||||
click = {
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
ModalManager.start.showCustomModal(keyboardCoversBar = false) { close ->
|
||||
val search = rememberSaveable { mutableStateOf("") }
|
||||
ModalView(
|
||||
{ close() },
|
||||
endButtons = {
|
||||
SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
|
||||
},
|
||||
showSearch = true,
|
||||
searchAlwaysVisible = true,
|
||||
onSearchValueChanged = { search.value = it },
|
||||
content = {
|
||||
ActiveProfilePicker(
|
||||
search = search,
|
||||
@@ -616,6 +625,7 @@ fun LinkTextView(link: String, share: Boolean) {
|
||||
enabled = false,
|
||||
isError = false,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
+1
-7
@@ -76,15 +76,9 @@ private fun CreateSimpleXAddressLayout(
|
||||
createAddress: () -> Unit,
|
||||
nextStep: () -> Unit,
|
||||
) {
|
||||
val handler = remember { AppBarHandler() }
|
||||
CompositionLocalProvider(
|
||||
LocalAppBarHandler provides handler
|
||||
) {
|
||||
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
|
||||
ModalView({}, showClose = false) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.themedBackground(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.simplex_address))
|
||||
|
||||
+1
-5
@@ -23,11 +23,7 @@ import dev.icerock.moko.resources.StringResource
|
||||
|
||||
@Composable
|
||||
fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>? = null) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(DEFAULT_PADDING),
|
||||
) {
|
||||
ColumnWithScrollBar(Modifier.padding(DEFAULT_PADDING)) {
|
||||
AppBarTitle(stringResource(MR.strings.how_simplex_works), withPadding = false)
|
||||
ReadableText(MR.strings.many_people_asked_how_can_it_deliver)
|
||||
ReadableText(MR.strings.to_protect_privacy_simplex_has_ids_for_queues)
|
||||
|
||||
+24
-31
@@ -7,21 +7,16 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.BackHandler
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.ui.theme.themedBackground
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.remote.AddingMobileDevice
|
||||
import chat.simplex.common.views.remote.DeviceNameField
|
||||
import chat.simplex.common.views.usersettings.PreferenceToggle
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
@@ -59,34 +54,32 @@ private fun LinkAMobileLayout(
|
||||
staleQrCode: MutableState<Boolean>,
|
||||
updateDeviceName: (String) -> Unit,
|
||||
) {
|
||||
Column(Modifier.themedBackground()) {
|
||||
CloseSheetBar(close = {
|
||||
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
})
|
||||
BackHandler(onBack = {
|
||||
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
})
|
||||
AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles))
|
||||
Row(Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING * 2), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(
|
||||
Modifier.weight(0.3f),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
SectionView(generalGetString(MR.strings.this_device_name).uppercase()) {
|
||||
DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) }
|
||||
SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile))
|
||||
PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), checked = remember { ChatModel.controller.appPrefs.offerRemoteMulticast.state }.value) {
|
||||
ChatModel.controller.appPrefs.offerRemoteMulticast.set(it)
|
||||
ModalView({ appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) }) {
|
||||
Column(Modifier.fillMaxSize().padding(top = AppBarHeight * fontSizeSqrtMultiplier)) {
|
||||
Box(Modifier.align(Alignment.CenterHorizontally)) {
|
||||
AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles))
|
||||
}
|
||||
Row(Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING * 2), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(
|
||||
Modifier.weight(0.3f),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
SectionView(generalGetString(MR.strings.this_device_name).uppercase()) {
|
||||
DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) }
|
||||
SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile))
|
||||
PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), checked = remember { ChatModel.controller.appPrefs.offerRemoteMulticast.state }.value) {
|
||||
ChatModel.controller.appPrefs.offerRemoteMulticast.set(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.weight(0.7f)) {
|
||||
AddingMobileDevice(false, staleQrCode, connecting) {
|
||||
// currentRemoteHost will be set instantly but remoteHosts may be delayed
|
||||
if (chatModel.remoteHosts.isEmpty() && chatModel.currentRemoteHost.value == null) {
|
||||
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
} else {
|
||||
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete)
|
||||
Box(Modifier.weight(0.7f)) {
|
||||
AddingMobileDevice(false, staleQrCode, connecting) {
|
||||
// currentRemoteHost will be set instantly but remoteHosts may be delayed
|
||||
if (chatModel.remoteHosts.isEmpty() && chatModel.currentRemoteHost.value == null) {
|
||||
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
} else {
|
||||
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-9
@@ -25,16 +25,9 @@ import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun SetNotificationsMode(m: ChatModel) {
|
||||
val handler = remember { AppBarHandler() }
|
||||
CompositionLocalProvider(
|
||||
LocalAppBarHandler provides handler
|
||||
) {
|
||||
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
|
||||
ModalView({}, showClose = false) {
|
||||
ColumnWithScrollBar(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.themedBackground()
|
||||
) {
|
||||
ColumnWithScrollBar(Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) {
|
||||
Box(Modifier.align(Alignment.CenterHorizontally)) {
|
||||
AppBarTitle(stringResource(MR.strings.onboarding_notifications_mode_title))
|
||||
}
|
||||
|
||||
+2
-5
@@ -104,13 +104,10 @@ private fun SetupDatabasePassphraseLayout(
|
||||
onConfirmEncrypt: () -> Unit,
|
||||
nextStep: () -> Unit,
|
||||
) {
|
||||
val handler = remember { AppBarHandler() }
|
||||
CompositionLocalProvider(
|
||||
LocalAppBarHandler provides handler
|
||||
) {
|
||||
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
|
||||
ModalView({}, showClose = false) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxSize().themedBackground().padding(bottom = DEFAULT_PADDING * 2),
|
||||
Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer).padding(bottom = DEFAULT_PADDING * 2),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.setup_database_passphrase))
|
||||
|
||||
+10
-9
@@ -31,15 +31,17 @@ import dev.icerock.moko.resources.StringResource
|
||||
@Composable
|
||||
fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) {
|
||||
if (onboarding) {
|
||||
ModalView({}, showClose = false, endButtons = {
|
||||
IconButton({ ModalManager.fullscreen.showModal { HowItWorks(chatModel.currentUser.value, null) }}) {
|
||||
Icon(painterResource(MR.images.ic_info), null, Modifier.size(28.dp), tint = MaterialTheme.colors.primary)
|
||||
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
|
||||
ModalView({}, showClose = false, endButtons = {
|
||||
IconButton({ ModalManager.fullscreen.showModal { HowItWorks(chatModel.currentUser.value, null) } }) {
|
||||
Icon(painterResource(MR.images.ic_info), null, Modifier.size(28.dp), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}) {
|
||||
SimpleXInfoLayout(
|
||||
user = chatModel.currentUser.value,
|
||||
onboardingStage = chatModel.controller.appPrefs.onboardingStage
|
||||
)
|
||||
}
|
||||
}) {
|
||||
SimpleXInfoLayout(
|
||||
user = chatModel.currentUser.value,
|
||||
onboardingStage = chatModel.controller.appPrefs.onboardingStage
|
||||
)
|
||||
}
|
||||
} else {
|
||||
SimpleXInfoLayout(
|
||||
@@ -56,7 +58,6 @@ fun SimpleXInfoLayout(
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
|
||||
+1
-2
@@ -119,11 +119,10 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
|
||||
ModalView(close = close) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING.times(0.75f))
|
||||
) {
|
||||
AppBarTitle(String.format(generalGetString(MR.strings.new_in_version), v.version), bottomPadding = DEFAULT_PADDING)
|
||||
AppBarTitle(String.format(generalGetString(MR.strings.new_in_version), v.version), withPadding = false, bottomPadding = DEFAULT_PADDING)
|
||||
|
||||
v.features.forEach { feature ->
|
||||
if (feature.show) {
|
||||
|
||||
+2
-6
@@ -74,9 +74,7 @@ private fun ConnectDesktopLayout(deviceName: String, close: () -> Unit) {
|
||||
val sessionAddress = remember { mutableStateOf("") }
|
||||
val remoteCtrls = remember { mutableStateListOf<RemoteCtrlInfo>() }
|
||||
val session = remember { chatModel.remoteCtrlSession }.value
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
val discovery = if (session == null) null else session.sessionState is UIRemoteCtrlSessionState.Searching
|
||||
if (discovery == true || (discovery == null && !showConnectScreen.value)) {
|
||||
SearchingDesktop(deviceName, remoteCtrls)
|
||||
@@ -408,9 +406,7 @@ private fun DesktopAddressView(sessionAddress: MutableState<String>) {
|
||||
|
||||
@Composable
|
||||
private fun LinkedDesktopsView(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.linked_desktops))
|
||||
SectionView(stringResource(MR.strings.desktop_devices).uppercase()) {
|
||||
remoteCtrls.forEach { rc ->
|
||||
|
||||
+10
-2
@@ -89,7 +89,7 @@ fun ConnectMobileLayout(
|
||||
connectDesktop: () -> Unit,
|
||||
deleteHost: (RemoteHostInfo) -> Unit,
|
||||
) {
|
||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles))
|
||||
SectionView(generalGetString(MR.strings.this_device_name).uppercase()) {
|
||||
DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) }
|
||||
@@ -176,7 +176,15 @@ private fun ConnectMobileViewLayout(
|
||||
refreshQrCode: () -> Unit = {},
|
||||
UnderQrLayout: @Composable () -> Unit = {},
|
||||
) {
|
||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
||||
@Composable
|
||||
fun ScrollableLayout(content: @Composable ColumnScope.() -> Unit) {
|
||||
if (LocalAppBarHandler.current != null) {
|
||||
ColumnWithScrollBar(content = content)
|
||||
} else {
|
||||
ColumnWithScrollBarNoAppBar(content = content)
|
||||
}
|
||||
}
|
||||
ScrollableLayout {
|
||||
if (title != null) {
|
||||
AppBarTitle(title)
|
||||
}
|
||||
|
||||
+3
-10
@@ -202,10 +202,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
|
||||
) {
|
||||
val secondsLabel = stringResource(MR.strings.network_option_seconds_label)
|
||||
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.network_settings_title))
|
||||
|
||||
if (currentRemoteHost == null) {
|
||||
@@ -328,9 +325,7 @@ private fun SMPProxyModePicker(
|
||||
icon = painterResource(MR.images.ic_settings_ethernet),
|
||||
onSelected = {
|
||||
showModal {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.network_smp_proxy_mode_private_routing))
|
||||
SectionViewSelectableCards(null, smpProxyMode, values, updateSMPProxyMode)
|
||||
}
|
||||
@@ -365,9 +360,7 @@ private fun SMPProxyFallbackPicker(
|
||||
enabled = enabled,
|
||||
onSelected = {
|
||||
showModal {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.network_smp_proxy_fallback_allow_downgrade))
|
||||
SectionViewSelectableCards(null, smpProxyFallback, values, updateSMPProxyFallback)
|
||||
}
|
||||
|
||||
+114
-29
@@ -4,9 +4,11 @@ import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionItemViewWithoutMinPadding
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -39,6 +41,7 @@ import chat.simplex.common.views.chat.item.msgTailWidthDp
|
||||
import chat.simplex.res.MR
|
||||
import com.godaddy.android.colorpicker.ClassicColorPicker
|
||||
import com.godaddy.android.colorpicker.HsvColor
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.datetime.Clock
|
||||
@@ -86,27 +89,114 @@ object AppearanceScope {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageShapeSection() {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_message_shape).uppercase(), contentPadding = PaddingValues()) {
|
||||
Row(modifier = Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING + 4.dp ) ,verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(stringResource(MR.strings.settings_message_shape_corner), color = colors.onBackground)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Slider(
|
||||
remember { appPreferences.chatItemRoundness.state }.value,
|
||||
valueRange = 0f..1f,
|
||||
steps = 20,
|
||||
onValueChange = {
|
||||
val diff = it % 0.05f
|
||||
appPreferences.chatItemRoundness.set(it + (if (diff >= 0.025f) -diff + 0.05f else -diff))
|
||||
saveThemeToDatabase(null)
|
||||
},
|
||||
colors = SliderDefaults.colors(
|
||||
activeTickColor = Color.Transparent,
|
||||
inactiveTickColor = Color.Transparent,
|
||||
fun AppToolbarsSection() {
|
||||
BoxWithConstraints {
|
||||
SectionView(stringResource(MR.strings.appearance_app_toolbars).uppercase()) {
|
||||
SectionItemViewWithoutMinPadding {
|
||||
Box(Modifier.weight(1f)) {
|
||||
Text(
|
||||
stringResource(MR.strings.appearance_in_app_bars_alpha),
|
||||
Modifier.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
) {
|
||||
appPrefs.inAppBarsAlpha.set(appPrefs.inAppBarsDefaultAlpha)
|
||||
},
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.padding(end = 10.dp))
|
||||
Slider(
|
||||
(1 - remember { appPrefs.inAppBarsAlpha.state }.value).coerceIn(0f, 0.5f),
|
||||
onValueChange = {
|
||||
val diff = it % 0.025f
|
||||
appPrefs.inAppBarsAlpha.set(1f - (String.format(Locale.US, "%.3f", it + (if (diff >= 0.0125f) -diff + 0.025f else -diff)).toFloatOrNull() ?: 1f))
|
||||
},
|
||||
Modifier.widthIn(max = (this@BoxWithConstraints.maxWidth - DEFAULT_PADDING * 2) * 0.618f),
|
||||
valueRange = 0f..0.5f,
|
||||
steps = 21,
|
||||
colors = SliderDefaults.colors(
|
||||
activeTickColor = Color.Transparent,
|
||||
inactiveTickColor = Color.Transparent,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
// In Android in OneHandUI there is a problem with setting initial value of blur if it was 0 before entering the screen.
|
||||
// So doing in two steps works ok
|
||||
fun saveBlur(value: Int) {
|
||||
val oneHandUI = appPrefs.oneHandUI.get()
|
||||
val pref = appPrefs.appearanceBarsBlurRadius
|
||||
if (appPlatform.isAndroid && oneHandUI && pref.get() == 0) {
|
||||
pref.set(if (value > 2) value - 1 else value + 1)
|
||||
withApi {
|
||||
delay(50)
|
||||
pref.set(value)
|
||||
}
|
||||
} else {
|
||||
pref.set(value)
|
||||
}
|
||||
}
|
||||
val blur = remember { appPrefs.appearanceBarsBlurRadius.state }
|
||||
if (appPrefs.deviceSupportsBlur || blur.value > 0) {
|
||||
SectionItemViewWithoutMinPadding {
|
||||
Box(Modifier.weight(1f)) {
|
||||
Text(
|
||||
stringResource(MR.strings.appearance_bars_blur_radius),
|
||||
Modifier.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
) {
|
||||
saveBlur(50)
|
||||
},
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.padding(end = 10.dp))
|
||||
Slider(
|
||||
blur.value.toFloat() / 100f,
|
||||
onValueChange = {
|
||||
val diff = it % 0.05f
|
||||
saveBlur(((String.format(Locale.US, "%.2f", it + (if (diff >= 0.025f) -diff + 0.05f else -diff)).toFloatOrNull() ?: 1f) * 100).toInt())
|
||||
},
|
||||
Modifier.widthIn(max = (this@BoxWithConstraints.maxWidth - DEFAULT_PADDING * 2) * 0.618f),
|
||||
valueRange = 0f..1f,
|
||||
steps = 21,
|
||||
colors = SliderDefaults.colors(
|
||||
activeTickColor = Color.Transparent,
|
||||
inactiveTickColor = Color.Transparent,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageShapeSection() {
|
||||
BoxWithConstraints {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_message_shape).uppercase()) {
|
||||
SectionItemViewWithoutMinPadding {
|
||||
Text(stringResource(MR.strings.settings_message_shape_corner), Modifier.weight(1f))
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Slider(
|
||||
remember { appPreferences.chatItemRoundness.state }.value,
|
||||
onValueChange = {
|
||||
val diff = it % 0.05f
|
||||
appPreferences.chatItemRoundness.set(it + (if (diff >= 0.025f) -diff + 0.05f else -diff))
|
||||
saveThemeToDatabase(null)
|
||||
},
|
||||
Modifier.widthIn(max = (this@BoxWithConstraints.maxWidth - DEFAULT_PADDING * 2) * 0.618f),
|
||||
valueRange = 0f..1f,
|
||||
steps = 20,
|
||||
colors = SliderDefaults.colors(
|
||||
activeTickColor = Color.Transparent,
|
||||
inactiveTickColor = Color.Transparent,
|
||||
)
|
||||
)
|
||||
}
|
||||
SettingsPreferenceItem(icon = null, stringResource(MR.strings.settings_message_shape_tail), appPreferences.chatItemTail)
|
||||
}
|
||||
SettingsPreferenceItem(icon = null, stringResource(MR.strings.settings_message_shape_tail), appPreferences.chatItemTail)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +205,7 @@ object AppearanceScope {
|
||||
val localFontScale = remember { mutableStateOf(appPrefs.fontScale.get()) }
|
||||
SectionView(stringResource(MR.strings.appearance_font_size).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(Modifier.size(60.dp)
|
||||
Box(Modifier.size(50.dp)
|
||||
.background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22))
|
||||
.clip(RoundedCornerShape(percent = 22))
|
||||
.clickable {
|
||||
@@ -129,7 +219,7 @@ object AppearanceScope {
|
||||
Text("Aa", color = if (localFontScale.value == 1f) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Spacer(Modifier.width(15.dp))
|
||||
// Text("${(localFontScale.value * 100).roundToInt()}%", Modifier.width(70.dp), textAlign = TextAlign.Center, fontSize = 12.sp)
|
||||
if (appPlatform.isAndroid) {
|
||||
Slider(
|
||||
@@ -185,7 +275,7 @@ object AppearanceScope {
|
||||
Column(Modifier
|
||||
.drawWithCache {
|
||||
if (wallpaperImage != null && wallpaperType != null && backgroundColor != null && tintColor != null) {
|
||||
chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor)
|
||||
chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor, null, null)
|
||||
} else {
|
||||
onDrawBehind {
|
||||
drawRect(themeBackgroundColor)
|
||||
@@ -514,9 +604,7 @@ object AppearanceScope {
|
||||
|
||||
@Composable
|
||||
fun CustomizeThemeView(onChooseType: (WallpaperType?) -> Unit) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
val currentTheme by CurrentColors.collectAsState()
|
||||
|
||||
AppBarTitle(stringResource(MR.strings.customize_theme_title))
|
||||
@@ -909,10 +997,7 @@ object AppearanceScope {
|
||||
currentColors: () -> ThemeManager.ActiveTheme,
|
||||
onColorChange: (Color?) -> Unit,
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
ColumnWithScrollBar(Modifier.imePadding()) {
|
||||
AppBarTitle(name.text)
|
||||
|
||||
val supportedLiveChange = name in listOf(ThemeColor.SECONDARY, ThemeColor.BACKGROUND, ThemeColor.SURFACE, ThemeColor.RECEIVED_MESSAGE, ThemeColor.SENT_MESSAGE, ThemeColor.SENT_QUOTE, ThemeColor.WALLPAPER_BACKGROUND, ThemeColor.WALLPAPER_TINT)
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ fun CallSettingsLayout(
|
||||
callOnLockScreen: SharedPreference<CallOnLockScreen>,
|
||||
editIceServers: () -> Unit,
|
||||
) {
|
||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.your_calls))
|
||||
val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) }
|
||||
SectionView(stringResource(MR.strings.settings_section_title_settings)) {
|
||||
|
||||
+4
-6
@@ -22,12 +22,10 @@ import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun DeveloperView(
|
||||
m: ChatModel,
|
||||
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
||||
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
|
||||
fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
|
||||
) {
|
||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
||||
val m = chatModel
|
||||
ColumnWithScrollBar {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
AppBarTitle(stringResource(MR.strings.settings_developer_tools))
|
||||
val developerTools = m.controller.appPrefs.developerTools
|
||||
@@ -35,7 +33,7 @@ fun DeveloperView(
|
||||
val unchangedHints = mutableStateOf(unchangedHintPreferences())
|
||||
SectionView {
|
||||
InstallTerminalAppItem(uriHandler)
|
||||
ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential), showCustomModal { it, close -> TerminalView(false, close) }) }
|
||||
ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.start.showModalCloseable { TerminalView(false) } } }
|
||||
ResetHintsItem(unchangedHints)
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_code), stringResource(MR.strings.show_developer_options), developerTools)
|
||||
SectionTextFooter(
|
||||
|
||||
+1
-5
@@ -21,11 +21,7 @@ fun HelpView(userDisplayName: String) {
|
||||
|
||||
@Composable
|
||||
fun HelpLayout(userDisplayName: String) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
){
|
||||
ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)){
|
||||
AppBarTitle(String.format(stringResource(MR.strings.personal_welcome), userDisplayName), withPadding = false)
|
||||
ChatHelpView()
|
||||
}
|
||||
|
||||
+1
-4
@@ -56,10 +56,7 @@ private fun HiddenProfileLayout(
|
||||
user: User,
|
||||
saveProfilePassword: (String) -> Unit
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.hide_profile))
|
||||
SectionView(contentPadding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
|
||||
UserProfileRow(user)
|
||||
|
||||
+3
-8
@@ -109,7 +109,7 @@ fun NetworkAndServersView() {
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
) {
|
||||
val m = chatModel
|
||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
||||
ColumnWithScrollBar {
|
||||
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) }
|
||||
val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.start.showCustomModal { close -> it(close) }}
|
||||
|
||||
@@ -304,10 +304,7 @@ fun SocksProxySettings(
|
||||
}
|
||||
},
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings))
|
||||
SectionView(stringResource(MR.strings.network_socks_proxy).uppercase()) {
|
||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||
@@ -479,9 +476,7 @@ fun SessionModePicker(
|
||||
icon = painterResource(MR.images.ic_safety_divider),
|
||||
onSelected = {
|
||||
showModal {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.network_session_mode_transport_isolation))
|
||||
SectionViewSelectable(null, sessionMode, values, updateSessionMode)
|
||||
}
|
||||
|
||||
+3
-9
@@ -56,9 +56,7 @@ fun NotificationsSettingsLayout(
|
||||
val modes = remember { notificationModes() }
|
||||
val previewModes = remember { notificationPreviewModes() }
|
||||
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.notifications))
|
||||
SectionView(null) {
|
||||
if (appPlatform == AppPlatform.ANDROID) {
|
||||
@@ -90,9 +88,7 @@ fun NotificationsModeView(
|
||||
onNotificationsModeSelected: (NotificationsMode) -> Unit,
|
||||
) {
|
||||
val modes = remember { notificationModes() }
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.settings_notifications_mode_title).lowercase().capitalize(Locale.current))
|
||||
SectionViewSelectable(null, notificationsMode, modes, onNotificationsModeSelected)
|
||||
}
|
||||
@@ -104,9 +100,7 @@ fun NotificationPreviewView(
|
||||
onNotificationPreviewModeSelected: (NotificationPreviewMode) -> Unit,
|
||||
) {
|
||||
val previewModes = remember { notificationPreviewModes() }
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.settings_notification_preview_title))
|
||||
SectionViewSelectable(null, notificationPreviewMode, previewModes, onNotificationPreviewModeSelected)
|
||||
}
|
||||
|
||||
+1
-3
@@ -66,9 +66,7 @@ private fun PreferencesLayout(
|
||||
reset: () -> Unit,
|
||||
savePrefs: () -> Unit,
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.your_preferences))
|
||||
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.allow) }
|
||||
TimedMessagesFeatureSection(timedMessages) {
|
||||
|
||||
+2
-6
@@ -55,9 +55,7 @@ fun PrivacySettingsView(
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
setPerformLA: (Boolean) -> Unit
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode
|
||||
AppBarTitle(stringResource(MR.strings.your_privacy))
|
||||
PrivacyDeviceSection(showSettingsModal, setPerformLA)
|
||||
@@ -514,9 +512,7 @@ fun SimplexLockView(
|
||||
}
|
||||
}
|
||||
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.chat_lock))
|
||||
SectionView {
|
||||
EnableLock(remember { appPrefs.performLA.state }) { performLAToggle ->
|
||||
|
||||
+1
-4
@@ -75,10 +75,7 @@ private fun ProtocolServerLayout(
|
||||
onUpdate: (ServerCfg) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(if (server.preset) MR.strings.smp_servers_preset_server else MR.strings.smp_servers_your_server))
|
||||
|
||||
if (server.preset) {
|
||||
|
||||
+1
-4
@@ -192,10 +192,7 @@ private fun ProtocolServersLayout(
|
||||
saveSMPServers: () -> Unit,
|
||||
showServer: (ServerCfg) -> Unit,
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.your_SMP_servers else MR.strings.your_XFTP_servers))
|
||||
|
||||
val configuredServers = servers.filter { it.preset || it.enabled }
|
||||
|
||||
+2
-4
@@ -7,6 +7,7 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress
|
||||
import chat.simplex.common.model.ServerCfg
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.QRCodeScanner
|
||||
@@ -17,10 +18,7 @@ expect fun ScanProtocolServer(rhId: Long?, onNext: (ServerCfg) -> Unit)
|
||||
|
||||
@Composable
|
||||
fun ScanProtocolServerLayout(rhId: Long?, onNext: (ServerCfg) -> Unit) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.smp_servers_scan_qr))
|
||||
QRCodeScanner { text ->
|
||||
val res = parseServerAddress(text)
|
||||
|
||||
+1
-4
@@ -74,10 +74,7 @@ private fun SetDeliveryReceiptsLayout(
|
||||
userCount: Int,
|
||||
) {
|
||||
Box(Modifier.padding(top = DEFAULT_PADDING)) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
ColumnWithScrollBar(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
AppBarTitle(stringResource(MR.strings.delivery_receipts_title))
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
+5
-15
@@ -39,7 +39,6 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: (
|
||||
val user = chatModel.currentUser.value
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
SettingsLayout(
|
||||
profile = user?.profile,
|
||||
stopped,
|
||||
chatModel.chatDbEncrypted.value == true,
|
||||
remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value,
|
||||
@@ -53,9 +52,9 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: (
|
||||
val search = rememberSaveable { mutableStateOf("") }
|
||||
ModalView(
|
||||
{ close() },
|
||||
endButtons = {
|
||||
SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
|
||||
},
|
||||
showSearch = true,
|
||||
searchAlwaysVisible = true,
|
||||
onSearchValueChanged = { search.value = it },
|
||||
content = { modalView(chatModel, search) })
|
||||
}
|
||||
},
|
||||
@@ -80,7 +79,6 @@ val simplexTeamUri =
|
||||
|
||||
@Composable
|
||||
fun SettingsLayout(
|
||||
profile: LocalProfile?,
|
||||
stopped: Boolean,
|
||||
encrypted: Boolean,
|
||||
passphraseSaved: Boolean,
|
||||
@@ -94,18 +92,12 @@ fun SettingsLayout(
|
||||
showVersion: () -> Unit,
|
||||
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val view = LocalMultiplatformView()
|
||||
LaunchedEffect(Unit) {
|
||||
hideKeyboard(view)
|
||||
}
|
||||
val theme = CurrentColors.collectAsState()
|
||||
val uriHandler = LocalUriHandler.current
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.themedBackground(theme.value.base)
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.your_settings))
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_settings)) {
|
||||
@@ -142,7 +134,7 @@ fun SettingsLayout(
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
SettingsSectionApp(showSettingsModal, showCustomModal, showVersion, withAuth)
|
||||
SettingsSectionApp(showSettingsModal, showVersion, withAuth)
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
@@ -150,7 +142,6 @@ fun SettingsLayout(
|
||||
@Composable
|
||||
expect fun SettingsSectionApp(
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
||||
showVersion: () -> Unit,
|
||||
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
|
||||
)
|
||||
@@ -488,7 +479,6 @@ private fun runAuth(title: String, desc: String, onFinish: (success: Boolean) ->
|
||||
fun PreviewSettingsLayout() {
|
||||
SimpleXTheme {
|
||||
SettingsLayout(
|
||||
profile = LocalProfile.sampleData,
|
||||
stopped = false,
|
||||
encrypted = false,
|
||||
passphraseSaved = false,
|
||||
|
||||
+1
-5
@@ -13,11 +13,7 @@ import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun UserAddressLearnMore() {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
ColumnWithScrollBar(Modifier .padding(horizontal = DEFAULT_PADDING)) {
|
||||
AppBarTitle(stringResource(MR.strings.simplex_address), withPadding = false)
|
||||
ReadableText(MR.strings.you_can_share_your_address)
|
||||
ReadableText(MR.strings.you_wont_lose_your_contacts_if_delete_address)
|
||||
|
||||
+1
-5
@@ -71,10 +71,8 @@ fun UserProfileLayout(
|
||||
val keyboardState by getKeyboardState()
|
||||
var savedKeyboardState by remember { mutableStateOf(keyboardState) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
ModalBottomSheetLayout(
|
||||
scrimColor = Color.Black.copy(alpha = 0.12F),
|
||||
modifier = Modifier.navigationBarsWithImePadding(),
|
||||
sheetContent = {
|
||||
GetImageBottomSheet(
|
||||
chosenImage,
|
||||
@@ -90,7 +88,6 @@ fun UserProfileLayout(
|
||||
displayName.value == profile.displayName &&
|
||||
fullName.value == profile.fullName &&
|
||||
profile.image == profileImage.value
|
||||
|
||||
val closeWithAlert = {
|
||||
if (dataUnchanged || !canSaveProfile(displayName.value, profile)) {
|
||||
close()
|
||||
@@ -103,7 +100,7 @@ fun UserProfileLayout(
|
||||
Modifier
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.your_current_profile))
|
||||
AppBarTitle(stringResource(MR.strings.your_current_profile), withPadding = false)
|
||||
ReadableText(generalGetString(MR.strings.your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it), TextAlign.Center)
|
||||
Column(
|
||||
Modifier
|
||||
@@ -170,7 +167,6 @@ fun UserProfileLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+2
-8
@@ -151,10 +151,7 @@ private fun UserProfilesLayout(
|
||||
unmuteUser: (User) -> Unit,
|
||||
showHiddenProfile: (User) -> Unit,
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
if (profileHidden.value) {
|
||||
SectionView {
|
||||
SettingsActionItem(painterResource(MR.images.ic_lock_open_right), stringResource(MR.strings.enter_password_to_show), click = {
|
||||
@@ -252,10 +249,7 @@ enum class UserProfileAction {
|
||||
|
||||
@Composable
|
||||
private fun ProfileActionView(action: UserProfileAction, user: User, doAction: (String) -> Unit) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
val actionPassword = rememberSaveable { mutableStateOf("") }
|
||||
val passwordValid by remember { derivedStateOf { actionPassword.value == actionPassword.value.trim() } }
|
||||
val actionEnabled by remember { derivedStateOf { actionPassword.value != "" && passwordValid && correctPassword(user, actionPassword.value) } }
|
||||
|
||||
+2
-2
@@ -1,6 +1,5 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -8,6 +7,7 @@ import androidx.compose.ui.Modifier
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.BuildConfigCommon
|
||||
import chat.simplex.common.model.CoreVersionInfo
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.views.helpers.AppBarTitle
|
||||
@@ -15,7 +15,7 @@ import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun VersionInfoView(info: CoreVersionInfo) {
|
||||
Column(
|
||||
ColumnWithScrollBar(
|
||||
Modifier.padding(horizontal = DEFAULT_PADDING),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.app_version_title), withPadding = false)
|
||||
|
||||
@@ -1757,6 +1757,9 @@
|
||||
<string name="theme_remove_image">Remove image</string>
|
||||
<string name="appearance_font_size">Font size</string>
|
||||
<string name="appearance_zoom">Zoom</string>
|
||||
<string name="appearance_app_toolbars">App toolbars</string>
|
||||
<string name="appearance_in_app_bars_alpha">Transparency</string>
|
||||
<string name="appearance_bars_blur_radius">Blur</string>
|
||||
<string name="system_mode_toast">System mode</string>
|
||||
|
||||
<!-- Wallpapers -->
|
||||
|
||||
Reference in New Issue
Block a user