Skip to content

Commit 2b6aaca

Browse files
Merge pull request #6608 from nextcloud/issue-6607-confirmation-prompt
Adding confirmation prompt on leave conversation action
2 parents ad15adf + 08188ef commit 2b6aaca

5 files changed

Lines changed: 88 additions & 39 deletions

File tree

app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ import android.widget.Toast
2323
import androidx.activity.OnBackPressedCallback
2424
import androidx.activity.compose.setContent
2525
import androidx.appcompat.app.AlertDialog
26+
import androidx.compose.material3.SnackbarDuration
2627
import androidx.compose.material3.SnackbarHostState
28+
import androidx.compose.material3.SnackbarResult
2729
import androidx.core.content.pm.ShortcutInfoCompat
2830
import androidx.core.content.pm.ShortcutManagerCompat
2931
import androidx.core.graphics.drawable.IconCompat
@@ -1167,7 +1169,7 @@ class ConversationsListActivity : BaseActivity() {
11671169
is ConversationOpsAction.Rename -> renameConversation(conversation)
11681170
is ConversationOpsAction.ToggleArchive -> handleArchiving(conversation)
11691171
is ConversationOpsAction.AddToHomeScreen -> addConversationToHomeScreen(conversation)
1170-
is ConversationOpsAction.Leave -> leaveConversation(conversation)
1172+
is ConversationOpsAction.Leave -> showLeaveConversationSnackbar(conversation)
11711173
is ConversationOpsAction.Delete -> showDeleteConversationDialog(conversation)
11721174
is ConversationOpsAction.ManageTags -> conversationTagsViewModel.setConversationForTagAssignment(
11731175
conversation
@@ -1223,10 +1225,33 @@ class ConversationsListActivity : BaseActivity() {
12231225
}
12241226
}
12251227

1228+
/**
1229+
* Rather than blocking with a confirmation dialog, hide the conversation immediately and offer
1230+
* an "Undo" snackbar. The actual leave-conversation network call is deferred until the snackbar
1231+
* goes away without being undone, so a room only needs to be rejoined if the user missed the
1232+
* undo window.
1233+
*/
12261234
@SuppressLint("StringFormatInvalid")
1235+
private fun showLeaveConversationSnackbar(conversation: ConversationModel) {
1236+
val token = conversation.token ?: return
1237+
conversationsListViewModel.markConversationPendingLeave(token)
1238+
lifecycleScope.launch {
1239+
val result = snackbarHostState.showSnackbar(
1240+
message = String.format(resources.getString(R.string.left_conversation), conversation.displayName),
1241+
actionLabel = getString(R.string.nc_undo),
1242+
duration = SnackbarDuration.Long
1243+
)
1244+
when (result) {
1245+
SnackbarResult.ActionPerformed -> conversationsListViewModel.clearConversationPendingLeave(token)
1246+
SnackbarResult.Dismissed -> leaveConversation(conversation)
1247+
}
1248+
}
1249+
}
1250+
12271251
private fun leaveConversation(conversation: ConversationModel) {
1252+
val token = conversation.token ?: return
12281253
val data = Data.Builder()
1229-
.putString(KEY_ROOM_TOKEN, conversation.token)
1254+
.putString(KEY_ROOM_TOKEN, token)
12301255
.putLong(KEY_INTERNAL_USER_ID, currentUser?.id!!)
12311256
.build()
12321257
val worker = OneTimeWorkRequest.Builder(LeaveConversationWorker::class.java)
@@ -1240,17 +1265,18 @@ class ConversationsListActivity : BaseActivity() {
12401265
currentUser?.id?.let { userId ->
12411266
ShortcutManagerHelper.disableConversationShortcut(
12421267
this,
1243-
conversation.token,
1268+
token,
12441269
userId,
12451270
resources.getString(R.string.nc_shortcut_conversation_deleted)
12461271
)
12471272
}
1248-
showSnackbar(
1249-
String.format(resources.getString(R.string.left_conversation), conversation.displayName)
1250-
)
1251-
startActivity(Intent(this, MainActivity::class.java))
1273+
conversationsListViewModel.clearConversationPendingLeave(token)
1274+
fetchRooms()
1275+
}
1276+
WorkInfo.State.FAILED -> {
1277+
conversationsListViewModel.clearConversationPendingLeave(token)
1278+
showSnackbar(resources.getString(R.string.nc_common_error_sorry))
12521279
}
1253-
WorkInfo.State.FAILED -> showSnackbar(resources.getString(R.string.nc_common_error_sorry))
12541280
else -> {}
12551281
}
12561282
}

app/src/main/java/com/nextcloud/talk/conversationlist/ui/ConversationListFab.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ private const val FAB_ANIM_DURATION = 200
4141
private const val UNREAD_MENTIONS_HORIZONTAL_SPACING = 88
4242

4343
@Composable
44-
fun ConversationListFab(isVisible: Boolean, isEnabled: Boolean, onClick: () -> Unit) {
44+
fun ConversationListFab(isVisible: Boolean, isEnabled: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
4545
AnimatedVisibility(
4646
visible = isVisible,
47+
modifier = modifier,
4748
enter = scaleIn(animationSpec = tween(FAB_ANIM_DURATION)) + fadeIn(animationSpec = tween(FAB_ANIM_DURATION)),
4849
exit = scaleOut(animationSpec = tween(FAB_ANIM_DURATION)) + fadeOut(animationSpec = tween(FAB_ANIM_DURATION))
4950
) {

app/src/main/java/com/nextcloud/talk/conversationlist/ui/ConversationsListScreen.kt

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ import kotlinx.coroutines.launch
7474

7575
private const val SEARCH_DEBOUNCE_MS = 300
7676
private const val SEARCH_MIN_CHARS = 1
77+
private const val OVERLAY_MARGIN = 16
7778

7879
@Suppress("LongParameterList")
7980
data class ConversationsListScreenState(
@@ -273,19 +274,6 @@ fun ConversationsListScreen(
273274
)
274275
)
275276
}
276-
},
277-
floatingActionButton = {
278-
ConversationListFab(
279-
isVisible = isFabVisible && !isSearchActive,
280-
isEnabled = isOnline,
281-
onClick = callbacks.onFabClick
282-
)
283-
},
284-
snackbarHost = {
285-
SnackbarHost(
286-
hostState = state.snackbarHostState,
287-
modifier = Modifier.navigationBarsPadding()
288-
)
289277
}
290278
) { paddingValues ->
291279
val layoutDirection = LocalLayoutDirection.current
@@ -386,15 +374,31 @@ fun ConversationsListScreen(
386374
}
387375
}
388376

389-
// Unread-mention bubble (bottom-center overlay)
390-
UnreadMentionBubble(
391-
visible = showUnreadBubble && !isSearchActive,
392-
onClick = callbacks.onUnreadBubbleClick,
377+
Column(
393378
modifier = Modifier
394379
.align(Alignment.BottomCenter)
395-
.navigationBarsPadding()
396-
.padding(bottom = 16.dp)
397-
)
380+
.fillMaxWidth()
381+
.padding(bottom = paddingValues.calculateBottomPadding())
382+
) {
383+
Box(modifier = Modifier.fillMaxWidth()) {
384+
UnreadMentionBubble(
385+
visible = showUnreadBubble && !isSearchActive,
386+
onClick = callbacks.onUnreadBubbleClick,
387+
modifier = Modifier
388+
.align(Alignment.BottomCenter)
389+
.padding(bottom = OVERLAY_MARGIN.dp)
390+
)
391+
ConversationListFab(
392+
isVisible = isFabVisible && !isSearchActive,
393+
isEnabled = isOnline,
394+
onClick = callbacks.onFabClick,
395+
modifier = Modifier
396+
.align(Alignment.BottomEnd)
397+
.padding(end = OVERLAY_MARGIN.dp, bottom = OVERLAY_MARGIN.dp)
398+
)
399+
}
400+
SnackbarHost(hostState = state.snackbarHostState)
401+
}
398402
}
399403

400404
// Account-chooser dialog

app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,13 @@ class ConversationsListViewModel @Inject constructor(
235235

236236
private val hideRoomToken = MutableStateFlow<String?>(null)
237237

238+
/** Tokens of rooms being left; hidden optimistically while the leave-undo snackbar is showing. */
239+
private val pendingLeaveTokens = MutableStateFlow<Set<String>>(emptySet())
240+
241+
private val excludedRoomTokens = combine(hideRoomToken, pendingLeaveTokens) { hideToken, pendingTokens ->
242+
if (hideToken != null) pendingTokens + hideToken else pendingTokens
243+
}
244+
238245
private enum class SearchDisplayMode {
239246
OFF,
240247
ALL_CONVERSATIONS,
@@ -272,9 +279,9 @@ class ConversationsListViewModel @Inject constructor(
272279
_filterStateFlow,
273280
searchDisplayModeFlow,
274281
combine(_selectedTagFilterFlow, selectedTagIsFavoritesFlow, ::TagFilterSelection),
275-
combine(searchResultEntries, hideRoomToken, ::Pair)
276-
) { rooms, filterState, searchMode, tagFilter, (searchResults, hideToken) ->
277-
buildConversationListEntries(rooms, filterState, searchMode, tagFilter, searchResults, hideToken)
282+
combine(searchResultEntries, excludedRoomTokens, ::Pair)
283+
) { rooms, filterState, searchMode, tagFilter, (searchResults, excludedTokens) ->
284+
buildConversationListEntries(rooms, filterState, searchMode, tagFilter, searchResults, excludedTokens)
278285
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
279286

280287
/**
@@ -288,9 +295,9 @@ class ConversationsListViewModel @Inject constructor(
288295
getRoomsStateFlow,
289296
_filterStateFlow,
290297
searchDisplayModeFlow,
291-
hideRoomToken
292-
) { rooms, filterState, searchMode, hideToken ->
293-
baseFilterRooms(rooms, filterState, searchMode, hideToken)
298+
excludedRoomTokens
299+
) { rooms, filterState, searchMode, excludedTokens ->
300+
baseFilterRooms(rooms, filterState, searchMode, excludedTokens)
294301
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
295302

296303
/** Clears the tag filter when the filtered-by tag no longer exists (e.g. it was deleted). */
@@ -373,6 +380,16 @@ class ConversationsListViewModel @Inject constructor(
373380
hideRoomToken.value = token
374381
}
375382

383+
/** Optimistically hide a room while its leave-undo snackbar is showing. */
384+
fun markConversationPendingLeave(token: String) {
385+
pendingLeaveTokens.value = pendingLeaveTokens.value + token
386+
}
387+
388+
/** Un-hide a room, either because the leave was undone or because it finished/failed. */
389+
fun clearConversationPendingLeave(token: String) {
390+
pendingLeaveTokens.value = pendingLeaveTokens.value - token
391+
}
392+
376393
fun getFederationInvitations() {
377394
_federationInvitationHintVisible.value = false
378395
_showAvatarBadge.value = false
@@ -654,11 +671,11 @@ class ConversationsListViewModel @Inject constructor(
654671
searchMode: SearchDisplayMode,
655672
tagFilter: TagFilterSelection,
656673
searchResults: List<ConversationListEntry>,
657-
hideToken: String?
674+
excludedTokens: Set<String>
658675
): List<ConversationListEntry> {
659676
if (searchMode == SearchDisplayMode.RESULTS) return searchResults
660677

661-
var filtered = baseFilterRooms(rooms, filterState, searchMode, hideToken)
678+
var filtered = baseFilterRooms(rooms, filterState, searchMode, excludedTokens)
662679

663680
if (searchMode != SearchDisplayMode.ALL_CONVERSATIONS) {
664681
filtered = when {
@@ -684,14 +701,14 @@ class ConversationsListViewModel @Inject constructor(
684701
rooms: List<ConversationModel>,
685702
filterState: Map<String, Boolean>,
686703
searchMode: SearchDisplayMode,
687-
hideToken: String?
704+
excludedTokens: Set<String>
688705
): List<ConversationModel> {
689706
val hasFilterEnabled = filterState[MENTION] == true ||
690707
filterState[UNREAD] == true ||
691708
filterState[ARCHIVE] == true
692709

693710
var filtered = rooms
694-
.filter { it.token != hideToken }
711+
.filter { it.token !in excludedTokens }
695712
.filter { conversation ->
696713
!(
697714
conversation.objectType == ConversationEnums.ObjectType.ROOM &&

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ How to translate with transifex:
334334

335335
<!-- Conversation menu -->
336336
<string name="nc_leave">Leave conversation</string>
337+
<string name="nc_undo">Undo</string>
337338
<string name="nc_clear_history">Delete all messages</string>
338339
<string name="nc_clear_history_warning">Do you really want to delete all messages in this conversation?</string>
339340
<string name="nc_clear_history_success">All messages were deleted</string>

0 commit comments

Comments
 (0)