mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-25 20:44:38 +00:00
android, desktop: new chat UI improvements
This commit is contained in:
+3
-1
@@ -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)
|
||||
}
|
||||
|
||||
+10
-1
@@ -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)
|
||||
|
||||
+15
@@ -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),
|
||||
|
||||
+101
-78
@@ -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 =
|
||||
|
||||
+2
-6
@@ -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>
|
||||
Reference in New Issue
Block a user