diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 269716564a..2291bc4d63 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -399,6 +399,13 @@ class ChatModel(val controller: ChatController) { fun contactNetworkStatus(contact: Contact): NetworkStatus = networkStatuses[contact.activeConn.agentConnId] ?: NetworkStatus.Unknown() + + fun addTerminalItem(item: TerminalItem) { + if (terminalItems.size >= 500) { + terminalItems.removeAt(0) + } + terminalItems.add(item) + } } enum class ChatType(val type: String) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index d00fcf4f75..820c045cbb 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -343,7 +343,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a return withContext(Dispatchers.IO) { val c = cmd.cmdString if (cmd !is CC.ApiParseMarkdown) { - chatModel.terminalItems.add(TerminalItem.cmd(cmd.obfuscated)) + chatModel.addTerminalItem(TerminalItem.cmd(cmd.obfuscated)) Log.d(TAG, "sendCmd: ${cmd.cmdType}") } val json = chatSendCmd(ctrl, c) @@ -353,7 +353,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a Log.d(TAG, "sendCmd response json $json") } if (r.resp !is CR.ParsedMarkdown) { - chatModel.terminalItems.add(TerminalItem.resp(r.resp)) + chatModel.addTerminalItem(TerminalItem.resp(r.resp)) } r.resp } @@ -1143,7 +1143,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a suspend fun processReceivedMsg(r: CR) { lastMsgReceivedTimestamp = System.currentTimeMillis() - chatModel.terminalItems.add(TerminalItem.resp(r)) + chatModel.addTerminalItem(TerminalItem.resp(r)) when (r) { is CR.NewContactConnection -> { if (active(r.user)) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index 18ec98fc7b..8f6d97d084 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -54,7 +54,7 @@ fun TerminalView(chatModel: ChatModel, close: () -> Unit) { lastSuccessfulAuth.value = SystemClock.elapsedRealtime() } TerminalLayout( - chatModel.terminalItems, + remember { chatModel.terminalItems }, composeState, sendCommand = { sendCommand(chatModel, composeState) }, close @@ -103,8 +103,8 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState) { onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } } val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed().toList() } } + val context = LocalContext.current LazyColumn(state = listState, reverseLayout = true) { items(reversedTerminalItems) { item -> Text( @@ -185,7 +186,7 @@ fun TerminalLog(terminalItems: List) { modifier = Modifier .fillMaxWidth() .clickable { - ModalManager.shared.showModal { + ModalManager.shared.showModal(endButtons = { ShareButton { shareText(context, item.details) } }) { SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) { Text(item.details, modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING)) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index d3d3d23534..3e6ec6ae84 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -65,6 +65,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: var searchInList by rememberSaveable { mutableStateOf("") } val scaffoldState = rememberScaffoldState() val scope = rememberCoroutineScope() + val switchingUsers = rememberSaveable { mutableStateOf(false) } Scaffold( topBar = { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } }, scaffoldState = scaffoldState, @@ -99,7 +100,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: ) { if (chatModel.chats.isNotEmpty()) { ChatList(chatModel, search = searchInList) - } else { + } else if (!switchingUsers.value) { Box(Modifier.fillMaxSize()) { if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) { OnboardingButtons(showNewChatSheet) @@ -113,9 +114,17 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: if (searchInList.isEmpty()) { NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet) } - UserPicker(chatModel, userPickerState) { + UserPicker(chatModel, userPickerState, switchingUsers) { scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } } + if (switchingUsers.value) { + Box( + Modifier.fillMaxSize().clickable(enabled = false, onClick = {}), + contentAlignment = Alignment.Center + ) { + ProgressIndicator() + } + } } @Composable @@ -267,6 +276,17 @@ private fun BoxScope.unreadBadge(text: String? = "") { ) } +@Composable +private fun ProgressIndicator() { + CircularProgressIndicator( + Modifier + .padding(horizontal = 2.dp) + .size(30.dp), + color = HighOrLowlight, + strokeWidth = 2.5.dp + ) +} + private var lazyListState = 0 to 0 @Composable diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/UserPicker.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/UserPicker.kt index 47a72f3c01..51ac86eab0 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/UserPicker.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/UserPicker.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.capitalize +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.* @@ -32,20 +33,11 @@ import kotlinx.coroutines.launch import kotlin.math.roundToInt @Composable -fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow, openSettings: () -> Unit) { +fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow, switchingUsers: MutableState, openSettings: () -> Unit) { val scope = rememberCoroutineScope() var newChat by remember { mutableStateOf(userPickerState.value) } val users by remember { derivedStateOf { chatModel.users.sortedByDescending { it.user.activeUser } } } val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) } - var progressIndicator by remember { mutableStateOf(false) } - if (progressIndicator) { - Box( - Modifier.fillMaxSize().clickable(enabled = false, onClick = {}), - contentAlignment = Alignment.Center - ) { - ProgressIndicator() - } - } LaunchedEffect(Unit) { launch { userPickerState.collect { @@ -113,11 +105,11 @@ fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow Unit = {}, onClick: () -> Unit) { - SectionItemViewSpaceBetween(onClick, onLongClick, padding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) { + Row( + Modifier + .fillMaxWidth() + .sizeIn(minHeight = 46.dp) + .combinedClickable( + onClick = if (!u.activeUser) onClick else { {} }, + onLongClick = onLongClick, + interactionSource = remember { MutableInteractionSource() }, + indication = if (!u.activeUser) LocalIndication.current else null + ) + .padding(PaddingValues(start = 8.dp, end = DEFAULT_PADDING)), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { Row( Modifier .widthIn(max = LocalConfiguration.current.screenWidthDp.dp * 0.7f) @@ -149,26 +154,30 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> Unit size = 54.dp ) Text( - u.chatViewName, + u.displayName, modifier = Modifier - .padding(start = 8.dp, end = 8.dp) + .padding(start = 8.dp, end = 8.dp), + fontWeight = if (u.activeUser) FontWeight.Medium else FontWeight.Normal ) } if (u.activeUser) { - Icon(Icons.Filled.Done, null, Modifier.size(20.dp), tint = MaterialTheme.colors.primary) + Icon(Icons.Filled.Done, null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) } else if (unreadCount > 0) { - Text( - unreadCountStr(unreadCount), - color = MaterialTheme.colors.onPrimary, - fontSize = 11.sp, - modifier = Modifier - .background(MaterialTheme.colors.primary, shape = CircleShape) - .sizeIn(minWidth = 20.dp, minHeight = 20.dp) - .padding(horizontal = 3.dp) - .padding(vertical = 1.dp), - textAlign = TextAlign.Center, - maxLines = 1 - ) + Row { + Text( + unreadCountStr(unreadCount), + color = MaterialTheme.colors.onPrimary, + fontSize = 11.sp, + modifier = Modifier + .background(MaterialTheme.colors.primary, shape = CircleShape) + .sizeIn(minWidth = 20.dp, minHeight = 20.dp) + .padding(horizontal = 3.dp) + .padding(vertical = 1.dp), + textAlign = TextAlign.Center, + maxLines = 1 + ) + Spacer(Modifier.width(2.dp)) + } } else { Box(Modifier.size(20.dp)) } @@ -186,14 +195,3 @@ private fun SettingsPickerItem(onClick: () -> Unit) { Icon(Icons.Outlined.Settings, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) } } - -@Composable -private fun ProgressIndicator() { - CircularProgressIndicator( - Modifier - .padding(horizontal = 2.dp) - .size(30.dp), - color = HighOrLowlight, - strokeWidth = 2.5.dp - ) -} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt index f62d8ffdc7..0d0d6675b3 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt @@ -11,7 +11,7 @@ import androidx.compose.ui.unit.dp import chat.simplex.app.ui.theme.* @Composable -fun CloseSheetBar(close: () -> Unit) { +fun CloseSheetBar(close: () -> Unit, endButtons: @Composable RowScope.() -> Unit = {}) { Column( Modifier .fillMaxWidth() @@ -20,9 +20,15 @@ fun CloseSheetBar(close: () -> Unit) { ) { Row( Modifier - .width(TitleInsetWithIcon - AppBarHorizontalPadding) .padding(top = 4.dp), // Like in DefaultAppBar - content = { NavigationButtonBack(close) } + content = { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + NavigationButtonBack(close) + Row { + endButtons() + } + } + } ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultTopAppBar.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultTopAppBar.kt index 601e258fce..827542c68a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultTopAppBar.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DefaultTopAppBar.kt @@ -53,6 +53,15 @@ fun NavigationButtonBack(onButtonClicked: () -> Unit) { } } +@Composable +fun ShareButton(onButtonClicked: () -> Unit) { + IconButton(onButtonClicked) { + Icon( + Icons.Outlined.Share, stringResource(R.string.share_verb), tint = MaterialTheme.colors.primary + ) + } +} + @Composable fun NavigationButtonMenu(onButtonClicked: () -> Unit) { IconButton(onClick = onButtonClicked) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt index 1ad24b60fd..5908f32a39 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt @@ -20,12 +20,13 @@ fun ModalView( close: () -> Unit, background: Color = MaterialTheme.colors.background, modifier: Modifier = Modifier, + endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit, ) { BackHandler(onBack = close) Surface(Modifier.fillMaxSize()) { Column(Modifier.background(background)) { - CloseSheetBar(close) + CloseSheetBar(close, endButtons) Box(modifier) { content() } } } @@ -37,9 +38,9 @@ class ModalManager { private val toRemove = mutableSetOf() private var oldViewChanging = AtomicBoolean(false) - fun showModal(settings: Boolean = false, content: @Composable () -> Unit) { + fun showModal(settings: Boolean = false, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit) { showCustomModal { close -> - ModalView(close, if (!settings || isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight, content = content) + ModalView(close, if (!settings || isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight, endButtons = endButtons, content = content) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt index b537339b06..d63989438c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt @@ -24,8 +24,6 @@ import androidx.compose.ui.graphics.* import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.capitalize -import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat @@ -65,11 +63,6 @@ fun AppearanceView() { AppearanceLayout( appIcon, changeIcon = ::setAppIcon, - showThemeSelector = { - ModalManager.shared.showModal(true) { - ThemeSelectorView() - } - }, editPrimaryColor = { primary -> ModalManager.shared.showModalCloseable { close -> ColorEditor(primary, close) @@ -81,7 +74,6 @@ fun AppearanceView() { @Composable fun AppearanceLayout( icon: MutableState, changeIcon: (AppIcon) -> Unit, - showThemeSelector: () -> Unit, editPrimaryColor: (Color) -> Unit, ) { Column( @@ -115,8 +107,12 @@ fun AppearanceView() { SectionSpacer() val currentTheme by CurrentColors.collectAsState() SectionView(stringResource(R.string.settings_section_title_themes)) { - SectionItemViewSpaceBetween(showThemeSelector) { - Text(generalGetString(R.string.theme)) + SectionItemViewSpaceBetween { + val darkTheme = isSystemInDarkTheme() + val state = remember { derivedStateOf { currentTheme.second } } + ThemeSelector(state) { + ThemeManager.applyTheme(it.name, darkTheme) + } } SectionDivider() SectionItemViewSpaceBetween({ editPrimaryColor(currentTheme.first.primary) }) { @@ -183,6 +179,21 @@ fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) { ) } +@Composable +private fun ThemeSelector(state: State, onSelected: (DefaultTheme) -> Unit) { + val darkTheme = isSystemInDarkTheme() + val values by remember { mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second to it.third }) } + ExposedDropDownSettingRow( + generalGetString(R.string.theme), + values, + state, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = onSelected + ) +} + + private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon -> SimplexApp.context.packageManager.getComponentEnabledSetting( ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${icon.name.lowercase()}") @@ -196,7 +207,6 @@ fun PreviewAppearanceSettings() { AppearanceLayout( icon = remember { mutableStateOf(AppIcon.DARK_BLUE) }, changeIcon = {}, - showThemeSelector = {}, editPrimaryColor = {}, ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ThemeSelector.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ThemeSelector.kt deleted file mode 100644 index 65b4ffed48..0000000000 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ThemeSelector.kt +++ /dev/null @@ -1,44 +0,0 @@ -package chat.simplex.app.views.usersettings - -import SectionViewSelectable -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.capitalize -import androidx.compose.ui.text.intl.Locale -import chat.simplex.app.R -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* - -@Composable -fun ThemeSelectorView() { - val darkTheme = isSystemInDarkTheme() - val allThemes by remember { mutableStateOf(ThemeManager.allThemes(darkTheme).map { ValueTitleDesc(it.second, it.third, "") }) } - - ThemeSelectorLayout( - allThemes, - onSelectTheme = { - ThemeManager.applyTheme(it.name, darkTheme) - }, - ) -} - -@Composable -private fun ThemeSelectorLayout( - allThemes: List>, - onSelectTheme: (DefaultTheme) -> Unit, -) { - Column( - Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.Start, - ) { - AppBarTitle(stringResource(R.string.settings_section_title_themes).lowercase().capitalize(Locale.current)) - val currentTheme by CurrentColors.collectAsState() - val state = remember { derivedStateOf { currentTheme.second } } - SectionViewSelectable(null, state, allThemes, onSelectTheme) - } -} -