From 4a5fdd3e0eed0d3eae2670bc7829d9ced073d892 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 26 Oct 2023 10:32:11 +0400 Subject: [PATCH] ios, android: show progress indicator on joining group (#3281) --- .../Chat/ChatItem/CIGroupInvitationView.swift | 68 +++++++++++------ .../Views/ChatList/ChatListNavLink.swift | 58 ++++++++++----- .../Views/ChatList/ChatPreviewView.swift | 19 +++-- .../simplex/common/views/chat/ChatView.kt | 21 +++--- .../views/chat/item/CIGroupInvitationView.kt | 74 +++++++++++++------ .../common/views/chat/item/ChatItemView.kt | 6 +- .../views/chatlist/ChatListNavLinkView.kt | 71 ++++++++++++++---- .../common/views/chatlist/ChatPreviewView.kt | 38 +++++++--- 8 files changed, 252 insertions(+), 103 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift index c7ec3ca71..72013877c 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift @@ -17,34 +17,45 @@ struct CIGroupInvitationView: View { var memberRole: GroupMemberRole var chatIncognito: Bool = false @State private var frameWidth: CGFloat = 0 + @State private var inProgress = false + @State private var progressByTimeout = false var body: some View { let action = !chatItem.chatDir.sent && groupInvitation.status == .pending let v = ZStack(alignment: .bottomTrailing) { - VStack(alignment: .leading) { - groupInfoView(action) - .padding(.horizontal, 2) - .padding(.top, 8) - .padding(.bottom, 6) - .overlay(DetermineWidth()) + ZStack { + VStack(alignment: .leading) { + groupInfoView(action) + .padding(.horizontal, 2) + .padding(.top, 8) + .padding(.bottom, 6) + .overlay(DetermineWidth()) - Divider().frame(width: frameWidth) + Divider().frame(width: frameWidth) - if action { - groupInvitationText() - .overlay(DetermineWidth()) - Text(chatIncognito ? "Tap to join incognito" : "Tap to join") - .foregroundColor(chatIncognito ? .indigo : .accentColor) - .font(.callout) - .padding(.trailing, 60) - .overlay(DetermineWidth()) - } else { - groupInvitationText() - .padding(.trailing, 60) - .overlay(DetermineWidth()) + if action { + VStack(alignment: .leading, spacing: 2) { + groupInvitationText() + .overlay(DetermineWidth()) + Text(chatIncognito ? "Tap to join incognito" : "Tap to join") + .foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : .accentColor) + .font(.callout) + .padding(.trailing, 60) + .overlay(DetermineWidth()) + } + } else { + groupInvitationText() + .padding(.trailing, 60) + .overlay(DetermineWidth()) + } + } + .padding(.bottom, 2) + + if progressByTimeout { + ProgressView().scaleEffect(2) } } - .padding(.bottom, 2) + chatItem.timestampText .font(.caption) .foregroundColor(.secondary) @@ -55,11 +66,24 @@ struct CIGroupInvitationView: View { .cornerRadius(18) .textSelection(.disabled) .onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 } + .onChange(of: inProgress) { inProgress in + if inProgress { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + progressByTimeout = inProgress + } + } else { + progressByTimeout = false + } + } if action { v.onTapGesture { - joinGroup(groupInvitation.groupId) + inProgress = true + joinGroup(groupInvitation.groupId) { + await MainActor.run { inProgress = false } + } } + .disabled(inProgress) } else { v } @@ -67,7 +91,7 @@ struct CIGroupInvitationView: View { private func groupInfoView(_ action: Bool) -> some View { var color: Color - if action { + if action && !inProgress { color = chatIncognito ? .indigo : .accentColor } else { color = Color(uiColor: .tertiaryLabel) diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 088335d19..971c0e088 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -33,19 +33,32 @@ struct ChatListNavLink: View { @State private var showContactConnectionInfo = false @State private var showInvalidJSON = false @State private var showDeleteContactActionSheet = false + @State private var inProgress = false + @State private var progressByTimeout = false var body: some View { - switch chat.chatInfo { - case let .direct(contact): - contactNavLink(contact) - case let .group(groupInfo): - groupNavLink(groupInfo) - case let .contactRequest(cReq): - contactRequestNavLink(cReq) - case let .contactConnection(cConn): - contactConnectionNavLink(cConn) - case let .invalidJSON(json): - invalidJSONPreview(json) + Group { + switch chat.chatInfo { + case let .direct(contact): + contactNavLink(contact) + case let .group(groupInfo): + groupNavLink(groupInfo) + case let .contactRequest(cReq): + contactRequestNavLink(cReq) + case let .contactConnection(cConn): + contactConnectionNavLink(cConn) + case let .invalidJSON(json): + invalidJSONPreview(json) + } + } + .onChange(of: inProgress) { inProgress in + if inProgress { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + progressByTimeout = inProgress + } + } else { + progressByTimeout = false + } } } @@ -53,7 +66,7 @@ struct ChatListNavLink: View { NavLinkPlain( tag: chat.chatInfo.id, selection: $chatModel.chatId, - label: { ChatPreviewView(chat: chat) } + label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) } ) .swipeActions(edge: .leading, allowsFullSwipe: true) { markReadButton() @@ -101,7 +114,7 @@ struct ChatListNavLink: View { @ViewBuilder private func groupNavLink(_ groupInfo: GroupInfo) -> some View { switch (groupInfo.membership.memberStatus) { case .memInvited: - ChatPreviewView(chat: chat) + ChatPreviewView(chat: chat, progressByTimeout: $progressByTimeout) .frame(height: rowHeights[dynamicTypeSize]) .swipeActions(edge: .trailing, allowsFullSwipe: true) { joinGroupButton() @@ -112,12 +125,16 @@ struct ChatListNavLink: View { .onTapGesture { showJoinGroupDialog = true } .confirmationDialog("Group invitation", isPresented: $showJoinGroupDialog, titleVisibility: .visible) { Button(chat.chatInfo.incognito ? "Join incognito" : "Join group") { - joinGroup(groupInfo.groupId) + inProgress = true + joinGroup(groupInfo.groupId) { + await MainActor.run { inProgress = false } + } } Button("Delete invitation", role: .destructive) { Task { await deleteChat(chat) } } } + .disabled(inProgress) case .memAccepted: - ChatPreviewView(chat: chat) + ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) .frame(height: rowHeights[dynamicTypeSize]) .onTapGesture { AlertManager.shared.showAlert(groupInvitationAcceptedAlert()) @@ -134,7 +151,7 @@ struct ChatListNavLink: View { NavLinkPlain( tag: chat.chatInfo.id, selection: $chatModel.chatId, - label: { ChatPreviewView(chat: chat) }, + label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) }, disabled: !groupInfo.ready ) .frame(height: rowHeights[dynamicTypeSize]) @@ -159,7 +176,10 @@ struct ChatListNavLink: View { private func joinGroupButton() -> some View { Button { - joinGroup(chat.chatInfo.apiId) + inProgress = true + joinGroup(chat.chatInfo.apiId) { + await MainActor.run { inProgress = false } + } } label: { Label("Join", systemImage: chat.chatInfo.incognito ? "theatermasks" : "ipad.and.arrow.forward") } @@ -419,7 +439,7 @@ func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, ) } -func joinGroup(_ groupId: Int64) { +func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) { Task { logger.debug("joinGroup") do { @@ -434,7 +454,9 @@ func joinGroup(_ groupId: Int64) { AlertManager.shared.showAlertMsg(title: "No group!", message: "This group no longer exists.") await deleteGroup() } + await onComplete() } catch let error { + await onComplete() let a = getErrorAlert(error, "Error joining group") AlertManager.shared.showAlertMsg(title: a.title, message: a.message) } diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index b7b7e73dc..71f8baf74 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -12,6 +12,7 @@ import SimpleXChat struct ChatPreviewView: View { @EnvironmentObject var chatModel: ChatModel @ObservedObject var chat: Chat + @Binding var progressByTimeout: Bool @Environment(\.colorScheme) var colorScheme var darkGreen = Color(red: 0, green: 0.5, blue: 0) @@ -252,6 +253,12 @@ struct ChatPreviewView: View { } else { incognitoIcon(chat.chatInfo.incognito) } + case .group: + if progressByTimeout { + ProgressView() + } else { + incognitoIcon(chat.chatInfo.incognito) + } default: incognitoIcon(chat.chatInfo.incognito) } @@ -280,30 +287,30 @@ struct ChatPreviewView_Previews: PreviewProvider { ChatPreviewView(chat: Chat( chatInfo: ChatInfo.sampleData.direct, chatItems: [] - )) + ), progressByTimeout: Binding.constant(false)) ChatPreviewView(chat: Chat( chatInfo: ChatInfo.sampleData.direct, chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))] - )) + ), progressByTimeout: Binding.constant(false)) ChatPreviewView(chat: Chat( chatInfo: ChatInfo.sampleData.direct, chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))], chatStats: ChatStats(unreadCount: 11, minUnreadItemId: 0) - )) + ), progressByTimeout: Binding.constant(false)) ChatPreviewView(chat: Chat( chatInfo: ChatInfo.sampleData.direct, chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now))] - )) + ), progressByTimeout: Binding.constant(false)) ChatPreviewView(chat: Chat( chatInfo: ChatInfo.sampleData.direct, chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))], chatStats: ChatStats(unreadCount: 3, minUnreadItemId: 0) - )) + ), progressByTimeout: Binding.constant(false)) ChatPreviewView(chat: Chat( chatInfo: ChatInfo.sampleData.group, chatItems: [ChatItem.getSample(1, .directSnd, .now, "Lorem ipsum dolor sit amet, d. consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")], chatStats: ChatStats(unreadCount: 11, minUnreadItemId: 0) - )) + ), progressByTimeout: Binding.constant(false)) } .previewLayout(.fixed(width: 360, height: 78)) } 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 40f2b32e5..94f9a6b54 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 @@ -269,8 +269,11 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: cancelFile = { fileId -> withApi { chatModel.controller.cancelFile(user, fileId) } }, - joinGroup = { groupId -> - withApi { chatModel.controller.apiJoinGroup(groupId) } + joinGroup = { groupId, onComplete -> + withApi { + chatModel.controller.apiJoinGroup(groupId) + onComplete.invoke() + } }, startCall = out@ { media -> withBGApi { @@ -431,7 +434,7 @@ fun ChatLayout( deleteMessage: (Long, CIDeleteMode) -> Unit, receiveFile: (Long, Boolean) -> Unit, cancelFile: (Long) -> Unit, - joinGroup: (Long) -> Unit, + joinGroup: (Long, () -> Unit) -> Unit, startCall: (CallMediaType) -> Unit, endCall: () -> Unit, acceptCall: (Contact) -> Unit, @@ -720,7 +723,7 @@ fun BoxWithConstraintsScope.ChatItemsList( deleteMessage: (Long, CIDeleteMode) -> Unit, receiveFile: (Long, Boolean) -> Unit, cancelFile: (Long) -> Unit, - joinGroup: (Long) -> Unit, + joinGroup: (Long, () -> Unit) -> Unit, acceptCall: (Contact) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, openDirectChat: (Long) -> Unit, @@ -872,7 +875,7 @@ fun BoxWithConstraintsScope.ChatItemsList( ) { MemberImage(member) } - ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames, developerTools = developerTools) + ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = { _, _ -> }, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames, developerTools = developerTools) } } } else { @@ -881,7 +884,7 @@ fun BoxWithConstraintsScope.ChatItemsList( .padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp) .then(swipeableModifier) ) { - ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames, developerTools = developerTools) + ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = { _, _ -> }, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames, developerTools = developerTools) } } } @@ -891,7 +894,7 @@ fun BoxWithConstraintsScope.ChatItemsList( .padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp) .then(swipeableModifier) ) { - ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools) + ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = { _, _ -> }, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools) } } } else { // direct message @@ -1323,7 +1326,7 @@ fun PreviewChatLayout() { deleteMessage = { _, _ -> }, receiveFile = { _, _ -> }, cancelFile = {}, - joinGroup = {}, + joinGroup = { _, _ -> }, startCall = {}, endCall = {}, acceptCall = { _ -> }, @@ -1393,7 +1396,7 @@ fun PreviewGroupChatLayout() { deleteMessage = { _, _ -> }, receiveFile = { _, _ -> }, cancelFile = {}, - joinGroup = {}, + joinGroup = { _, _ -> }, startCall = {}, endCall = {}, acceptCall = { _ -> }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt index 6ee29b830..56dd7a360 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.* 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 dev.icerock.moko.resources.compose.stringResource @@ -17,6 +18,7 @@ import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* import chat.simplex.res.MR +import kotlinx.coroutines.delay @Composable fun CIGroupInvitationView( @@ -24,16 +26,26 @@ fun CIGroupInvitationView( groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole, chatIncognito: Boolean = false, - joinGroup: (Long) -> Unit + joinGroup: (Long, () -> Unit) -> Unit ) { val sent = ci.chatDir.sent val action = !sent && groupInvitation.status == CIGroupInvitationStatus.Pending + val inProgress = remember { mutableStateOf(false) } + var progressByTimeout by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(inProgress.value) { + progressByTimeout = if (inProgress.value) { + delay(1000) + inProgress.value + } else { + false + } + } @Composable fun groupInfoView() { val p = groupInvitation.groupProfile val iconColor = - if (action) if (chatIncognito) Indigo else MaterialTheme.colors.primary + if (action && !inProgress.value) if (chatIncognito) Indigo else MaterialTheme.colors.primary else if (isInDarkTheme()) FileDark else FileLight Row( @@ -70,8 +82,9 @@ fun CIGroupInvitationView( val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage Surface( - modifier = if (action) Modifier.clickable(onClick = { - joinGroup(groupInvitation.groupId) + modifier = if (action && !inProgress.value) Modifier.clickable(onClick = { + inProgress.value = true + joinGroup(groupInvitation.groupId) { inProgress.value = false } }) else Modifier, shape = RoundedCornerShape(18.dp), color = if (sent) sentColor else receivedColor, @@ -83,26 +96,45 @@ fun CIGroupInvitationView( .padding(start = 8.dp, end = 12.dp), contentAlignment = Alignment.BottomEnd ) { - Column( - Modifier - .defaultMinSize(minWidth = 220.dp) - .padding(bottom = 4.dp), + Box( + contentAlignment = Alignment.Center ) { - groupInfoView() - Column(Modifier.padding(top = 2.dp, start = 5.dp)) { - Divider(Modifier.fillMaxWidth().padding(bottom = 4.dp)) - if (action) { - groupInvitationText() - Text(stringResource( - if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join), - color = if (chatIncognito) Indigo else MaterialTheme.colors.primary) - } else { - Box(Modifier.padding(end = 48.dp)) { + Column( + Modifier + .defaultMinSize(minWidth = 220.dp) + .padding(bottom = 4.dp), + ) { + groupInfoView() + Column(Modifier.padding(top = 2.dp, start = 5.dp)) { + Divider(Modifier.fillMaxWidth().padding(bottom = 4.dp)) + if (action) { groupInvitationText() + Text( + stringResource( + if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join + ), + color = if (inProgress.value) + MaterialTheme.colors.secondary + else + if (chatIncognito) Indigo else MaterialTheme.colors.primary + ) + } else { + Box(Modifier.padding(end = 48.dp)) { + groupInvitationText() + } } } } + + if (progressByTimeout) { + CircularProgressIndicator( + Modifier.size(32.dp), + color = if (isInDarkTheme()) FileDark else FileLight, + strokeWidth = 3.dp + ) + } } + Text( ci.timestampText, color = MaterialTheme.colors.secondary, @@ -124,7 +156,7 @@ fun PendingCIGroupInvitationViewPreview() { ci = ChatItem.getGroupInvitationSample(), groupInvitation = CIGroupInvitation.getSample(), memberRole = GroupMemberRole.Admin, - joinGroup = {} + joinGroup = { _, _ -> } ) } } @@ -140,7 +172,7 @@ fun CIGroupInvitationViewAcceptedPreview() { ci = ChatItem.getGroupInvitationSample(), groupInvitation = CIGroupInvitation.getSample(status = CIGroupInvitationStatus.Accepted), memberRole = GroupMemberRole.Admin, - joinGroup = {} + joinGroup = { _, _ -> } ) } } @@ -156,7 +188,7 @@ fun CIGroupInvitationViewLongNamePreview() { status = CIGroupInvitationStatus.Accepted ), memberRole = GroupMemberRole.Admin, - joinGroup = {} + joinGroup = { _, _ -> } ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index dd07a3fc1..dd9fe4d4a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -50,7 +50,7 @@ fun ChatItemView( deleteMessage: (Long, CIDeleteMode) -> Unit, receiveFile: (Long, Boolean) -> Unit, cancelFile: (Long) -> Unit, - joinGroup: (Long) -> Unit, + joinGroup: (Long, () -> Unit) -> Unit, acceptCall: (Contact) -> Unit, scrollToItem: (Long) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, @@ -578,7 +578,7 @@ fun PreviewChatItemView() { deleteMessage = { _, _ -> }, receiveFile = { _, _ -> }, cancelFile = {}, - joinGroup = {}, + joinGroup = { _, _ -> }, acceptCall = { _ -> }, scrollToItem = {}, acceptFeature = { _, _, _ -> }, @@ -609,7 +609,7 @@ fun PreviewChatItemViewDeletedContent() { deleteMessage = { _, _ -> }, receiveFile = { _, _ -> }, cancelFile = {}, - joinGroup = {}, + joinGroup = { _, _ -> }, acceptCall = { _ -> }, scrollToItem = {}, acceptFeature = { _, _, _ -> }, 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 2ff33ead5..bcabb7cfd 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 @@ -12,6 +12,7 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -44,11 +45,22 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { } val selectedChat = remember(chat.id) { derivedStateOf { chat.id == ChatModel.chatId.value } } val showChatPreviews = chatModel.showChatPreviews.value + val inProgress = remember { mutableStateOf(false) } + var progressByTimeout by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(inProgress.value) { + progressByTimeout = if (inProgress.value) { + delay(1000) + inProgress.value + } else { + false + } + } + when (chat.chatInfo) { is ChatInfo.Direct -> { val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact) ChatListNavLinkLayout( - chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode) }, + chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode, inProgress = false, progressByTimeout = false) }, click = { directChatAction(chat.chatInfo, chatModel) }, dropdownMenuItems = { ContactMenuItems(chat, chatModel, showMenu, showMarkRead) }, showMenu, @@ -58,9 +70,9 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { } is ChatInfo.Group -> ChatListNavLinkLayout( - chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode) }, - click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) }, - dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, showMarkRead) }, + chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode, inProgress.value, progressByTimeout) }, + click = { if (!inProgress.value) groupChatAction(chat.chatInfo.groupInfo, chatModel, inProgress) }, + dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, inProgress, showMarkRead) }, showMenu, stopped, selectedChat @@ -110,9 +122,9 @@ fun directChatAction(chatInfo: ChatInfo, chatModel: ChatModel) { withBGApi { openChat(chatInfo, chatModel) } } -fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel) { +fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState? = null) { when (groupInfo.membership.memberStatus) { - GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(groupInfo, chatModel) + GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(groupInfo, chatModel, inProgress) GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert() else -> withBGApi { openChat(ChatInfo.Group(groupInfo), chatModel) } } @@ -193,10 +205,19 @@ fun ContactMenuItems(chat: Chat, chatModel: ChatModel, showMenu: MutableState, showMarkRead: Boolean) { +fun GroupMenuItems( + chat: Chat, + groupInfo: GroupInfo, + chatModel: ChatModel, + showMenu: MutableState, + inProgress: MutableState, + showMarkRead: Boolean +) { when (groupInfo.membership.memberStatus) { GroupMemberStatus.MemInvited -> { - JoinGroupAction(chat, groupInfo, chatModel, showMenu) + if (!inProgress.value) { + JoinGroupAction(chat, groupInfo, chatModel, showMenu, inProgress) + } if (groupInfo.canDelete) { DeleteGroupAction(chat, groupInfo, chatModel, showMenu) } @@ -317,8 +338,20 @@ fun DeleteGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, sh } @Composable -fun JoinGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState) { - val joinGroup: () -> Unit = { withApi { chatModel.controller.apiJoinGroup(groupInfo.groupId) } } +fun JoinGroupAction( + chat: Chat, + groupInfo: GroupInfo, + chatModel: ChatModel, + showMenu: MutableState, + inProgress: MutableState +) { + val joinGroup: () -> Unit = { + withApi { + inProgress.value = true + chatModel.controller.apiJoinGroup(groupInfo.groupId) + inProgress.value = false + } + } ItemAction( if (chat.chatInfo.incognito) stringResource(MR.strings.join_group_incognito_button) else stringResource(MR.strings.join_group_button), if (chat.chatInfo.incognito) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_login), @@ -558,12 +591,18 @@ fun pendingContactAlertDialog(chatInfo: ChatInfo, chatModel: ChatModel) { ) } -fun acceptGroupInvitationAlertDialog(groupInfo: GroupInfo, chatModel: ChatModel) { +fun acceptGroupInvitationAlertDialog(groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState? = null) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.join_group_question), text = generalGetString(MR.strings.you_are_invited_to_group_join_to_connect_with_group_members), confirmText = if (groupInfo.membership.memberIncognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), - onConfirm = { withApi { chatModel.controller.apiJoinGroup(groupInfo.groupId) } }, + onConfirm = { + withApi { + inProgress?.value = true + chatModel.controller.apiJoinGroup(groupInfo.groupId) + inProgress?.value = false + } + }, dismissText = generalGetString(MR.strings.delete_verb), onDismiss = { deleteGroup(groupInfo, chatModel) } ) @@ -680,7 +719,9 @@ fun PreviewChatListNavLinkDirect() { null, null, stopped = false, - linkMode = SimplexLinkMode.DESCRIPTION + linkMode = SimplexLinkMode.DESCRIPTION, + inProgress = false, + progressByTimeout = false ) }, click = {}, @@ -721,7 +762,9 @@ fun PreviewChatListNavLinkGroup() { null, null, stopped = false, - linkMode = SimplexLinkMode.DESCRIPTION + linkMode = SimplexLinkMode.DESCRIPTION, + inProgress = false, + progressByTimeout = false ) }, click = {}, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index a5775d369..d3413e2e0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -37,7 +37,9 @@ fun ChatPreviewView( currentUserProfileDisplayName: String?, contactNetworkStatus: NetworkStatus?, stopped: Boolean, - linkMode: SimplexLinkMode + linkMode: SimplexLinkMode, + inProgress: Boolean, + progressByTimeout: Boolean ) { val cInfo = chat.chatInfo @@ -135,7 +137,12 @@ fun ChatPreviewView( } is ChatInfo.Group -> when (cInfo.groupInfo.membership.memberStatus) { - GroupMemberStatus.MemInvited -> chatPreviewTitleText(if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary) + GroupMemberStatus.MemInvited -> chatPreviewTitleText( + if (inProgress) + MaterialTheme.colors.secondary + else + if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary + ) GroupMemberStatus.MemAccepted -> chatPreviewTitleText(MaterialTheme.colors.secondary) else -> chatPreviewTitleText() } @@ -194,6 +201,17 @@ fun ChatPreviewView( } } + @Composable + fun progressView() { + CircularProgressIndicator( + Modifier + .padding(horizontal = 2.dp) + .size(15.dp), + color = MaterialTheme.colors.secondary, + strokeWidth = 1.5.dp + ) + } + @Composable fun chatStatusImage() { if (cInfo is ChatInfo.Direct) { @@ -213,17 +231,17 @@ fun ChatPreviewView( ) else -> - CircularProgressIndicator( - Modifier - .padding(horizontal = 2.dp) - .size(15.dp), - color = MaterialTheme.colors.secondary, - strokeWidth = 1.5.dp - ) + progressView() } } else { IncognitoIcon(chat.chatInfo.incognito) } + } else if (cInfo is ChatInfo.Group) { + if (progressByTimeout) { + progressView() + } else { + IncognitoIcon(chat.chatInfo.incognito) + } } else { IncognitoIcon(chat.chatInfo.incognito) } @@ -351,6 +369,6 @@ fun unreadCountStr(n: Int): String { @Composable fun PreviewChatPreviewView() { SimpleXTheme { - ChatPreviewView(Chat.sampleData, true, null, null, "", contactNetworkStatus = NetworkStatus.Connected(), stopped = false, linkMode = SimplexLinkMode.DESCRIPTION) + ChatPreviewView(Chat.sampleData, true, null, null, "", contactNetworkStatus = NetworkStatus.Connected(), stopped = false, linkMode = SimplexLinkMode.DESCRIPTION, inProgress = false, progressByTimeout = false) } }