diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 273400854..8dfa5e05f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -38,6 +38,7 @@ import chat.simplex.common.views.chatlist.updateChatSettings import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import kotlinx.datetime.Clock @Composable @@ -51,15 +52,16 @@ fun ChatInfoView( close: () -> Unit, ) { BackHandler(onBack = close) - val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value } - val currentUser = chatModel.currentUser.value - val connStats = remember { mutableStateOf(connectionStats) } + val contact = rememberUpdatedState(contact).value + val chat = remember(contact.id) { chatModel.chats.firstOrNull { it.id == contact.id } } + val currentUser = remember { chatModel.currentUser }.value + val connStats = remember(contact.id, connectionStats) { mutableStateOf(connectionStats) } val developerTools = chatModel.controller.appPrefs.developerTools.get() if (chat != null && currentUser != null) { - val contactNetworkStatus = remember(chatModel.networkStatuses.toMap()) { + val contactNetworkStatus = remember(chatModel.networkStatuses.toMap(), contact) { mutableStateOf(chatModel.contactNetworkStatus(contact)) } - val sendReceipts = remember { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) } + val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) } ChatInfoLayout( chat, contact, @@ -239,7 +241,7 @@ fun ChatInfoLayout( currentUser: User, sendReceipts: State, setSendReceipts: (SendReceipts) -> Unit, - connStats: MutableState, + connStats: State, contactNetworkStatus: NetworkStatus, customUserProfile: Profile?, localAlias: String, @@ -256,10 +258,15 @@ fun ChatInfoLayout( verifyClicked: () -> Unit, ) { val cStats = connStats.value + val scrollState = rememberScrollState() + val scope = rememberCoroutineScope() + KeyChangeEffect(chat.id) { + scope.launch { scrollState.scrollTo(0) } + } Column( Modifier .fillMaxWidth() - .verticalScroll(rememberScrollState()) + .verticalScroll(scrollState) ) { Row( Modifier.fillMaxWidth(), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 70825e6fb..7d2787827 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -133,27 +133,45 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) { chatModel.chatId.value = null }, info = { + if (ModalManager.end.hasModalsOpen()) { + ModalManager.end.closeModals() + return@ChatLayout + } hideKeyboard(view) withApi { + // The idea is to preload information before showing a modal because large groups can take time to load all members + var preloadedContactInfo: Pair? = null + var preloadedCode: String? = null + var preloadedLink: Pair? = null if (chat.chatInfo is ChatInfo.Direct) { - val contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId) - val (_, code) = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId) - ModalManager.end.closeModals() - ModalManager.end.showModalCloseable(true) { close -> - remember { derivedStateOf { (chatModel.getContactChat(chat.chatInfo.apiId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct -> - ChatInfoView(chatModel, ct, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close) - } - } + preloadedContactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId) + preloadedCode = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId).second } else if (chat.chatInfo is ChatInfo.Group) { setGroupMembers(chat.chatInfo.groupInfo, chatModel) - val link = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId) - var groupLink = link?.first - var groupLinkMemberRole = link?.second - ModalManager.end.closeModals() - ModalManager.end.showModalCloseable(true) { close -> - GroupChatInfoView(chatModel, groupLink, groupLinkMemberRole, { - groupLink = it.first; - groupLinkMemberRole = it.second + preloadedLink = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId) + } + ModalManager.end.showModalCloseable(true) { close -> + val chat = remember { activeChat }.value + if (chat?.chatInfo is ChatInfo.Direct) { + var contactInfo: Pair? by remember { mutableStateOf(preloadedContactInfo) } + var code: String? by remember { mutableStateOf(preloadedCode) } + KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) { + contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId) + preloadedContactInfo = contactInfo + code = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId).second + preloadedCode = code + } + ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close) + } else if (chat?.chatInfo is ChatInfo.Group) { + var link: Pair? by remember(chat.id) { mutableStateOf(preloadedLink) } + KeyChangeEffect(chat.id) { + setGroupMembers((chat.chatInfo as ChatInfo.Group).groupInfo, chatModel) + link = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId) + preloadedLink = link + } + GroupChatInfoView(chatModel, link?.first, link?.second, { + link = it + preloadedLink = it }, close) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt index 1b8310e18..8eeaa8dc0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt @@ -71,6 +71,9 @@ fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, ch removeContact = { contactId -> selectedContacts.removeIf { it == contactId } }, close = close, ) + KeyChangeEffect(chatModel.chatId.value) { + close() + } } fun getContactsToAdd(chatModel: ChatModel, search: String): List { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 402a9f28c..e5f63ed3e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -8,8 +8,8 @@ import SectionSpacer import SectionTextFooter import SectionView import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -33,11 +33,12 @@ import chat.simplex.common.platform.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.chatlist.* import chat.simplex.res.MR +import kotlinx.coroutines.launch const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20 @Composable -fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair) -> Unit, close: () -> Unit) { +fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair?) -> Unit, close: () -> Unit) { BackHandler(onBack = close) val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value } val currentUser = chatModel.currentUser.value @@ -177,79 +178,92 @@ fun GroupChatInfoLayout( leaveGroup: () -> Unit, manageGroupLink: () -> Unit, ) { - Column( + val listState = rememberLazyListState() + val scope = rememberCoroutineScope() + KeyChangeEffect(chat.id) { + scope.launch { listState.scrollToItem(0) } + } + val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) } + val filteredMembers = remember(members) { derivedStateOf { members.filter { it.chatViewName.lowercase().contains(searchText.value.text.trim()) } } } + LazyColumn( Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) + .fillMaxWidth(), + state = listState ) { - Row( - Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - GroupChatInfoHeader(chat.chatInfo) - } - SectionSpacer() + item { + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + GroupChatInfoHeader(chat.chatInfo) + } + SectionSpacer() - SectionView { - if (groupInfo.canEdit) { - EditGroupProfileButton(editGroupProfile) - } - if (groupInfo.groupProfile.description != null || groupInfo.canEdit) { - AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage) - } - GroupPreferencesButton(openPreferences) - if (members.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) { - SendReceiptsOption(currentUser, sendReceipts, setSendReceipts) - } else { - SendReceiptsOptionDisabled() - } - } - SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs)) - SectionDividerSpaced(maxTopPadding = true) - - SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), members.count() + 1)) { - if (groupInfo.canAddMembers) { - if (groupLink == null) { - CreateGroupLinkButton(manageGroupLink) + SectionView { + if (groupInfo.canEdit) { + EditGroupProfileButton(editGroupProfile) + } + if (groupInfo.groupProfile.description != null || groupInfo.canEdit) { + AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage) + } + GroupPreferencesButton(openPreferences) + if (members.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) { + SendReceiptsOption(currentUser, sendReceipts, setSendReceipts) } else { - GroupLinkButton(manageGroupLink) - } - - val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers - val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary - AddMembersButton(tint, onAddMembersClick) - } - val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) } - val filteredMembers = remember { derivedStateOf { members.filter { it.chatViewName.lowercase().contains(searchText.value.text.trim()) } } } - if (members.size > 8) { - SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) { - SearchRowView(searchText) + SendReceiptsOptionDisabled() } } - SectionItemView(minHeight = 54.dp) { - MemberRow(groupInfo.membership, user = true) - } - MembersList(filteredMembers.value, showMemberInfo) - } - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) - SectionView { - ClearChatButton(clearChat) - if (groupInfo.canDelete) { - DeleteGroupButton(deleteGroup) - } - if (groupInfo.membership.memberCurrent) { - LeaveGroupButton(leaveGroup) - } - } + SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs)) + SectionDividerSpaced(maxTopPadding = true) - if (developerTools) { - SectionDividerSpaced() - SectionView(title = stringResource(MR.strings.section_title_for_console)) { - InfoRow(stringResource(MR.strings.info_row_local_name), groupInfo.localDisplayName) - InfoRow(stringResource(MR.strings.info_row_database_id), groupInfo.apiId.toString()) + SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), members.count() + 1)) { + if (groupInfo.canAddMembers) { + if (groupLink == null) { + CreateGroupLinkButton(manageGroupLink) + } else { + GroupLinkButton(manageGroupLink) + } + val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers + val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + AddMembersButton(tint, onAddMembersClick) + } + if (members.size > 8) { + SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) { + SearchRowView(searchText) + } + } + SectionItemView(minHeight = 54.dp) { + MemberRow(groupInfo.membership, user = true) + } } } - SectionBottomSpacer() + items(filteredMembers.value) { member -> + Divider() + SectionItemView({ showMemberInfo(member) }, minHeight = 54.dp) { + MemberRow(member) + } + } + item { + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + SectionView { + ClearChatButton(clearChat) + if (groupInfo.canDelete) { + DeleteGroupButton(deleteGroup) + } + if (groupInfo.membership.memberCurrent) { + LeaveGroupButton(leaveGroup) + } + } + + if (developerTools) { + SectionDividerSpaced() + SectionView(title = stringResource(MR.strings.section_title_for_console)) { + InfoRow(stringResource(MR.strings.info_row_local_name), groupInfo.localDisplayName) + InfoRow(stringResource(MR.strings.info_row_database_id), groupInfo.apiId.toString()) + } + } + SectionBottomSpacer() + } } } @@ -330,18 +344,6 @@ private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, onClick ) } -@Composable -private fun MembersList(members: List, showMemberInfo: (GroupMember) -> Unit) { - Column { - members.forEachIndexed { index, member -> - Divider() - SectionItemView({ showMemberInfo(member) }, minHeight = 54.dp) { - MemberRow(member) - } - } - } -} - @Composable private fun MemberRow(member: GroupMember, user: Boolean = false) { Row( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt index 22b9c8a93..7c767f9b7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt @@ -23,7 +23,7 @@ import chat.simplex.common.views.newchat.QRCode import chat.simplex.res.MR @Composable -fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, memberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair) -> Unit) { +fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, memberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair?) -> Unit) { var groupLink by rememberSaveable { mutableStateOf(connReqContact) } val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) } var creatingLink by rememberSaveable { mutableStateOf(false) } @@ -34,7 +34,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St if (link != null) { groupLink = link.first groupLinkMemberRole.value = link.second - onGroupLinkUpdated(groupLink to groupLinkMemberRole.value) + onGroupLinkUpdated(link) } creatingLink = false } @@ -60,7 +60,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St if (link != null) { groupLink = link.first groupLinkMemberRole.value = link.second - onGroupLinkUpdated(groupLink to groupLinkMemberRole.value) + onGroupLinkUpdated(link) } } } @@ -75,7 +75,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId) if (r) { groupLink = null - onGroupLinkUpdated(null to null) + onGroupLinkUpdated(null) } } }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index f92473749..5452bbba2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -368,7 +368,12 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel: stringResource(MR.strings.delete_verb), painterResource(MR.images.ic_delete), onClick = { - deleteContactConnectionAlert(chatInfo.contactConnection, chatModel) {} + deleteContactConnectionAlert(chatInfo.contactConnection, chatModel) { + if (chatModel.chatId.value == null) { + ModalManager.center.closeModals() + ModalManager.end.closeModals() + } + } showMenu.value = false }, color = Color.Red diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index e2c316046..f71ec865f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -61,6 +61,13 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf connectIfOpenedViaUri(url, chatModel) } } + if (appPlatform.isDesktop) { + KeyChangeEffect(chatModel.chatId.value) { + if (chatModel.chatId.value != null) { + ModalManager.end.closeModalsExceptFirst() + } + } + } val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp var searchInList by rememberSaveable { mutableStateOf("") } val scope = rememberCoroutineScope() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt index ec3ee8ece..0f930b312 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt @@ -97,6 +97,12 @@ class ModalManager(private val placement: ModalPlacement? = null) { modalCount.value = 0 } + fun closeModalsExceptFirst() { + while (modalCount.value > 1) { + closeModal() + } + } + @OptIn(ExperimentalAnimationApi::class) @Composable fun showInView() { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index 781adbd8e..f8c85405e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -329,3 +329,43 @@ fun DisposableEffectOnRotate(always: () -> Unit = {}, whenDispose: () -> Unit = } } } + +/** + * Runs the [block] only after initial value of the [key1] changes, not after initial launch + * */ +@Composable +@NonRestartableComposable +fun KeyChangeEffect( + key1: Any?, + block: suspend CoroutineScope.() -> Unit +) { + val initialKey = remember { key1 } + var anyChange by remember { mutableStateOf(false) } + LaunchedEffect(key1) { + if (anyChange || key1 != initialKey) { + block() + anyChange = true + } + } +} + +/** + * Runs the [block] only after initial value of the [key1] or [key2] changes, not after initial launch + * */ +@Composable +@NonRestartableComposable +fun KeyChangeEffect( + key1: Any?, + key2: Any?, + block: suspend CoroutineScope.() -> Unit +) { + val initialKey = remember { key1 } + val initialKey2 = remember { key2 } + var anyChange by remember { mutableStateOf(false) } + LaunchedEffect(key1) { + if (anyChange || key1 != initialKey || key2 != initialKey2) { + block() + anyChange = true + } + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt index 3a76808b0..5aeddd299 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt @@ -40,7 +40,7 @@ actual fun screenOrientation(): ScreenOrientation = ScreenOrientation.UNDEFINED @Composable // LALAL actual fun screenWidth(): Dp { - return java.awt.Toolkit.getDefaultToolkit().screenSize.width.dp.also { println("LALAL $it") } + return java.awt.Toolkit.getDefaultToolkit().screenSize.width.dp /*var width by remember { mutableStateOf(java.awt.Toolkit.getDefaultToolkit().screenSize.width.also { println("LALAL $it") }) } SideEffect { if (width != java.awt.Toolkit.getDefaultToolkit().screenSize.width)