mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-27 22:34:51 +00:00
android: user picker UI changes + share button (#1858)
* android: user picker UI changes + share button * limit terminal items size in a different way
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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<Compose
|
||||
val s = composeState.value.message
|
||||
if (s.startsWith("/sql") && (!prefPerformLA || !developerTools)) {
|
||||
val resp = CR.ChatCmdError(null, ChatError.ChatErrorChat(ChatErrorType.СommandError("Failed reading: empty")))
|
||||
chatModel.terminalItems.add(TerminalItem.cmd(CC.Console(s)))
|
||||
chatModel.terminalItems.add(TerminalItem.resp(resp))
|
||||
chatModel.addTerminalItem(TerminalItem.cmd(CC.Console(s)))
|
||||
chatModel.addTerminalItem(TerminalItem.resp(resp))
|
||||
composeState.value = ComposeState(useLinkPreviews = false)
|
||||
} else {
|
||||
withApi {
|
||||
@@ -175,6 +175,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
|
||||
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<TerminalItem>) {
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<AnimatedViewState>, openSettings: () -> Unit) {
|
||||
fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, switchingUsers: MutableState<Boolean>, 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<AnimatedV
|
||||
scope.launch {
|
||||
val job = launch {
|
||||
delay(500)
|
||||
progressIndicator = true
|
||||
switchingUsers.value = true
|
||||
}
|
||||
chatModel.controller.changeActiveUser(u.user.userId)
|
||||
job.cancel()
|
||||
progressIndicator = false
|
||||
switchingUsers.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,7 +129,20 @@ fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedV
|
||||
|
||||
@Composable
|
||||
fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> 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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<Int>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<AppIcon>,
|
||||
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<DefaultTheme>, 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 = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<ValueTitleDesc<DefaultTheme>>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user