android, desktop: new chat UI improvements

This commit is contained in:
Diogo Cunha
2024-07-15 23:24:21 +01:00
parent 3133d01690
commit 69ad380245
6 changed files with 133 additions and 87 deletions
@@ -662,7 +662,7 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) {
}
}
fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel, onSuccess: () -> Unit = {}) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.accept_connection_request__question),
text = AnnotatedString(generalGetString(MR.strings.if_you_choose_to_reject_the_sender_will_not_be_notified)),
@@ -671,12 +671,14 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque
SectionItemView({
AlertManager.shared.hideAlert()
acceptContactRequest(rhId, incognito = false, contactRequest.apiId, contactRequest, true, chatModel)
onSuccess()
}) {
Text(generalGetString(MR.strings.accept_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
SectionItemView({
AlertManager.shared.hideAlert()
acceptContactRequest(rhId, incognito = true, contactRequest.apiId, contactRequest, true, chatModel)
onSuccess()
}) {
Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
@@ -54,7 +54,16 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHand
ContactPreviewView(chat, disabled)
}
},
click = { contactRequestAlertDialog(chat.remoteHostId, chat.chatInfo, chatModel) },
click = {
contactRequestAlertDialog(
chat.remoteHostId,
chat.chatInfo,
chatModel,
onSuccess = {
ModalManager.start.closeModals()
}
)
},
dropdownMenuItems = {
tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) {
ContactRequestMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu)
@@ -3,6 +3,7 @@ package chat.simplex.common.views.contacts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -99,6 +100,19 @@ fun ContactPreviewView(
Spacer(Modifier.fillMaxWidth().weight(1f))
if (chat.chatInfo is ChatInfo.ContactRequest) {
Text(
text = generalGetString(MR.strings.contact_type_new).uppercase(),
color = MaterialTheme.colors.onPrimary,
fontSize = 10.sp * fontSizeMultiplier,
modifier = Modifier
.background(MaterialTheme.colors.primary, shape = CircleShape)
.badgeLayout()
.padding(horizontal = 4.dp)
.padding(vertical = 2.dp)
)
}
if (chat.chatInfo.chatSettings?.favorite == true) {
Icon(
painterResource(MR.images.ic_star_filled),
@@ -112,6 +126,7 @@ fun ContactPreviewView(
}
}
if (chat.chatInfo.incognito) {
Icon(
painterResource(MR.images.ic_theater_comedy),
@@ -1,11 +1,15 @@
package chat.simplex.common.views.contacts
import SectionDividerSpaced
import SectionItemView
import SectionView
import TextIconSpaced
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -24,6 +28,7 @@ import androidx.compose.foundation.pager.rememberPagerState
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.draw.scale
@@ -35,6 +40,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.toUpperCase
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -57,6 +63,7 @@ import chat.simplex.common.platform.chatModel
import chat.simplex.common.platform.getKeyboardState
import chat.simplex.common.platform.hideKeyboard
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chatlist.DetailedSMPStatsView
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.strHasSingleSimplexLink
import chat.simplex.res.MR
@@ -64,110 +71,121 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import java.util.HashSet
enum class ContactType {
RECENT, NEW, REMOVED
CARD, REQUEST, RECENT, CONNECTED_VIA_GROUP, REMOVED, UNKNOWN
}
private fun contactChats(c: List<Chat>, contactType: ContactType): List<Chat> {
return c.filter { chat ->
when (val cInfo = chat.chatInfo) {
is ChatInfo.Direct -> when (contactType) {
ContactType.REMOVED -> cInfo.contact.contactStatus == ContactStatus.DeletedByUser || cInfo.contact.contactStatus == ContactStatus.Deleted
ContactType.NEW -> false
ContactType.RECENT -> cInfo.contact.contactStatus != ContactStatus.DeletedByUser && cInfo.contact.contactStatus != ContactStatus.Deleted
private fun contactChats(c: List<Chat>, contactTypes: List<ContactType>): List<Chat> {
return c.filter { chat -> contactTypes.contains(getContactType(chat)) }
}
private fun getContactType(chat: Chat): ContactType {
return when (val cInfo = chat.chatInfo) {
is ChatInfo.ContactRequest -> ContactType.REQUEST
is ChatInfo.Direct -> {
val contact = cInfo.contact;
when {
contact.activeConn == null && contact.profile.contactLink != null -> ContactType.CARD
contact.contactStatus == ContactStatus.DeletedByUser || contact.contactStatus == ContactStatus.Deleted -> ContactType.REMOVED
else -> ContactType.RECENT
}
is ChatInfo.ContactRequest -> when (contactType) {
ContactType.NEW -> true
else -> false
}
is ChatInfo.Group -> {
when {
cInfo.groupInfo.sendMsgEnabled -> ContactType.CONNECTED_VIA_GROUP
else -> ContactType.UNKNOWN
}
}
else -> ContactType.UNKNOWN
}
}
val chatsByTypeComparator = Comparator<Chat> { chat1, chat2 ->
val chat1Type = getContactType(chat1)
val chat2Type = getContactType(chat2)
when {
chat1Type.ordinal < chat2Type.ordinal -> -1
chat1Type.ordinal > chat2Type.ordinal -> 1
else -> chat2.chatInfo.chatTs.compareTo(chat1.chatInfo.chatTs)
}
}
@Composable
fun ContactActionsSection(contactActions: @Composable () -> Unit) {
contactActions()
Spacer(Modifier.height(DEFAULT_PADDING))
val archived = remember { contactChats(chatModel.chats, listOf(ContactType.REMOVED)) }
if (archived.isNotEmpty()) {
SectionView {
SectionItemView(
click = {
//
}
) {
Icon(
painterResource(MR.images.ic_folder_open),
contentDescription = stringResource(MR.strings.contact_type_deleted),
tint = MaterialTheme.colors.secondary,
)
TextIconSpaced(extraPadding = true)
Text(text = stringResource(MR.strings.contact_type_deleted), color = MaterialTheme.colors.onBackground)
}
else -> false
}
}
}
@Composable
fun ContactTypeTabs(contactActions: @Composable () -> Unit, searchText: MutableState<TextFieldValue>) {
val scope = rememberCoroutineScope()
val selectedContactType =
remember { mutableStateOf(ContactType.RECENT) }
val contactTypeTabTitles = ContactType.entries.map {
when (it) {
ContactType.NEW ->
stringResource(MR.strings.contact_type_new)
ContactType.RECENT ->
stringResource(MR.strings.contact_type_recent)
ContactType.REMOVED ->
stringResource(MR.strings.contact_type_removed)
}
}
val contactTypePagerState = rememberPagerState(
initialPage = selectedContactType.value.ordinal,
initialPageOffsetFraction = 0f
) { ContactType.entries.size }
KeyChangeEffect(contactTypePagerState.currentPage) {
selectedContactType.value = ContactType.values()[contactTypePagerState.currentPage]
}
fun ContactsView(contactActions: @Composable () -> Unit) {
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
var searchFocused by remember { mutableStateOf(false) }
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(
TextFieldValue("")
) }
SectionView {
Divider()
ContactsSearchBar(listState, searchText)
ContactsSearchBar(
listState = listState,
searchText = searchText,
focused = searchFocused,
onFocusChanged = {
searchFocused = it
}
)
Divider()
}
contactActions()
TabRow(
selectedTabIndex = contactTypePagerState.currentPage,
backgroundColor = Color.Transparent,
contentColor = MaterialTheme.colors.primary,
) {
contactTypeTabTitles.forEachIndexed { index, it ->
Tab(
selected = contactTypePagerState.currentPage == index,
onClick = {
scope.launch {
contactTypePagerState.animateScrollToPage(index)
}
},
text = { Text(it, fontSize = 13.sp) },
selectedContentColor = MaterialTheme.colors.primary,
unselectedContentColor = MaterialTheme.colors.secondary,
)
}
if (!searchFocused) {
ContactActionsSection(contactActions)
}
HorizontalPager(
state = contactTypePagerState,
Modifier.fillMaxSize(),
verticalAlignment = Alignment.Top,
userScrollEnabled = appPlatform.isAndroid
) { index ->
val contactType = when (index) {
ContactType.NEW.ordinal -> ContactType.NEW
ContactType.RECENT.ordinal -> ContactType.RECENT
ContactType.REMOVED.ordinal -> ContactType.REMOVED
else -> ContactType.RECENT
}
Spacer(Modifier.height(DEFAULT_PADDING))
ContactsList(listState = listState, chatModel = chatModel, searchText = searchText, contactType = contactType)
SectionView(title = stringResource(MR.strings.contact_list_header_title).uppercase(), padding = PaddingValues(DEFAULT_PADDING)) {
ContactsList(listState = listState, chatModel = chatModel, searchText = searchText, contactType = ContactType.RECENT)
}
if (searchFocused) {
ContactActionsSection(contactActions)
}
}
@Composable
fun ContactsSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>) {
fun ContactsSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, focused: Boolean, onFocusChanged: (hasFocus: Boolean) -> Unit) {
Row(verticalAlignment = Alignment.CenterVertically) {
val focusRequester = remember { FocusRequester() }
var focused by remember { mutableStateOf(false) }
Icon(painterResource(MR.images.ic_search), null, Modifier.padding(horizontal = DEFAULT_PADDING_HALF), tint = MaterialTheme.colors.secondary)
SearchTextField(
Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester),
Modifier.weight(1f).onFocusChanged { onFocusChanged(it.hasFocus) }.focusRequester(focusRequester),
placeholder = stringResource(MR.strings.search_verb),
alwaysVisible = true,
searchText = searchText,
@@ -245,7 +263,7 @@ fun ContactsList(listState: LazyListState, chatModel: ChatModel, searchText: Mut
}
val showUnreadAndFavorites =
remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value
val allChats = remember { contactChats(chatModel.chats, contactType) }
val allChats = remember { contactChats(chatModel.chats, listOf(ContactType.CARD, ContactType.RECENT, ContactType.REQUEST)) }
val filteredContactChats = filteredContactChats(
showUnreadAndFavorites = showUnreadAndFavorites,
@@ -302,14 +320,19 @@ private fun filteredContactChats(
searchText: String,
contactChats: List<Chat>
): List<Chat> {
val s = searchText.trim().lowercase()
return contactChats
.filter { chat -> filterChat(
chat = chat,
searchText = searchText,
showUnreadAndFavorites = showUnreadAndFavorites) }
.sortedByDescending { it.chatInfo.chatTs }
.distinctBy { chat ->
when (val cInfo = chat.chatInfo) {
is ChatInfo.ContactRequest -> cInfo.contactRequest.contactRequestId
is ChatInfo.Direct -> cInfo.contact.contactId
else -> cInfo.id
}
}
.sortedWith(chatsByTypeComparator)
}
private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean =
@@ -32,7 +32,7 @@ import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.RemoteHostInfo
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.contacts.ContactTypeTabs
import chat.simplex.common.views.contacts.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.flow.MutableStateFlow
@@ -53,12 +53,8 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?) {
bottomPadding = bottomPadding
)
}
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(
TextFieldValue("")
) }
ContactTypeTabs(
searchText = searchText,
ContactsView(
contactActions = {
NewChatOptions(
addContact = {
@@ -2217,7 +2217,8 @@
<!-- ContactsView.kt -->
<string name="contact_type_new">New</string>
<string name="contact_type_removed">Removed</string>
<string name="contact_type_deleted">Deleted contacts</string>
<string name="contact_type_recent">Recent</string>
<string name="no_filtered_contacts">No filtered contacts</string>
<string name="contact_list_header_title">Your contacts</string>
</resources>