Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
import androidx.appcompat.app.AlertDialog
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
Expand Down Expand Up @@ -1167,7 +1169,7 @@ class ConversationsListActivity : BaseActivity() {
is ConversationOpsAction.Rename -> renameConversation(conversation)
is ConversationOpsAction.ToggleArchive -> handleArchiving(conversation)
is ConversationOpsAction.AddToHomeScreen -> addConversationToHomeScreen(conversation)
is ConversationOpsAction.Leave -> leaveConversation(conversation)
is ConversationOpsAction.Leave -> showLeaveConversationSnackbar(conversation)
is ConversationOpsAction.Delete -> showDeleteConversationDialog(conversation)
is ConversationOpsAction.ManageTags -> conversationTagsViewModel.setConversationForTagAssignment(
conversation
Expand Down Expand Up @@ -1223,10 +1225,33 @@ class ConversationsListActivity : BaseActivity() {
}
}

/**
* Rather than blocking with a confirmation dialog, hide the conversation immediately and offer
* an "Undo" snackbar. The actual leave-conversation network call is deferred until the snackbar
* goes away without being undone, so a room only needs to be rejoined if the user missed the
* undo window.
*/
@SuppressLint("StringFormatInvalid")
private fun showLeaveConversationSnackbar(conversation: ConversationModel) {
val token = conversation.token ?: return
conversationsListViewModel.markConversationPendingLeave(token)
lifecycleScope.launch {
val result = snackbarHostState.showSnackbar(
message = String.format(resources.getString(R.string.left_conversation), conversation.displayName),
actionLabel = getString(R.string.nc_undo),
duration = SnackbarDuration.Long
)
when (result) {
SnackbarResult.ActionPerformed -> conversationsListViewModel.clearConversationPendingLeave(token)
SnackbarResult.Dismissed -> leaveConversation(conversation)
}
}
}

private fun leaveConversation(conversation: ConversationModel) {
val token = conversation.token ?: return
val data = Data.Builder()
.putString(KEY_ROOM_TOKEN, conversation.token)
.putString(KEY_ROOM_TOKEN, token)
.putLong(KEY_INTERNAL_USER_ID, currentUser?.id!!)
.build()
val worker = OneTimeWorkRequest.Builder(LeaveConversationWorker::class.java)
Expand All @@ -1240,17 +1265,18 @@ class ConversationsListActivity : BaseActivity() {
currentUser?.id?.let { userId ->
ShortcutManagerHelper.disableConversationShortcut(
this,
conversation.token,
token,
userId,
resources.getString(R.string.nc_shortcut_conversation_deleted)
)
}
showSnackbar(
String.format(resources.getString(R.string.left_conversation), conversation.displayName)
)
startActivity(Intent(this, MainActivity::class.java))
conversationsListViewModel.clearConversationPendingLeave(token)
fetchRooms()
}
WorkInfo.State.FAILED -> {
conversationsListViewModel.clearConversationPendingLeave(token)
showSnackbar(resources.getString(R.string.nc_common_error_sorry))
}
WorkInfo.State.FAILED -> showSnackbar(resources.getString(R.string.nc_common_error_sorry))
else -> {}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ private const val FAB_ANIM_DURATION = 200
private const val UNREAD_MENTIONS_HORIZONTAL_SPACING = 88

@Composable
fun ConversationListFab(isVisible: Boolean, isEnabled: Boolean, onClick: () -> Unit) {
fun ConversationListFab(isVisible: Boolean, isEnabled: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
AnimatedVisibility(
visible = isVisible,
modifier = modifier,
enter = scaleIn(animationSpec = tween(FAB_ANIM_DURATION)) + fadeIn(animationSpec = tween(FAB_ANIM_DURATION)),
exit = scaleOut(animationSpec = tween(FAB_ANIM_DURATION)) + fadeOut(animationSpec = tween(FAB_ANIM_DURATION))
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import kotlinx.coroutines.launch

private const val SEARCH_DEBOUNCE_MS = 300
private const val SEARCH_MIN_CHARS = 1
private const val OVERLAY_MARGIN = 16

@Suppress("LongParameterList")
data class ConversationsListScreenState(
Expand Down Expand Up @@ -273,19 +274,6 @@ fun ConversationsListScreen(
)
)
}
},
floatingActionButton = {
ConversationListFab(
isVisible = isFabVisible && !isSearchActive,
isEnabled = isOnline,
onClick = callbacks.onFabClick
)
},
snackbarHost = {
SnackbarHost(
hostState = state.snackbarHostState,
modifier = Modifier.navigationBarsPadding()
)
}
) { paddingValues ->
val layoutDirection = LocalLayoutDirection.current
Expand Down Expand Up @@ -386,15 +374,31 @@ fun ConversationsListScreen(
}
}

// Unread-mention bubble (bottom-center overlay)
UnreadMentionBubble(
visible = showUnreadBubble && !isSearchActive,
onClick = callbacks.onUnreadBubbleClick,
Column(
modifier = Modifier
.align(Alignment.BottomCenter)
.navigationBarsPadding()
.padding(bottom = 16.dp)
)
.fillMaxWidth()
.padding(bottom = paddingValues.calculateBottomPadding())
) {
Box(modifier = Modifier.fillMaxWidth()) {
UnreadMentionBubble(
visible = showUnreadBubble && !isSearchActive,
onClick = callbacks.onUnreadBubbleClick,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = OVERLAY_MARGIN.dp)
)
ConversationListFab(
isVisible = isFabVisible && !isSearchActive,
isEnabled = isOnline,
onClick = callbacks.onFabClick,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = OVERLAY_MARGIN.dp, bottom = OVERLAY_MARGIN.dp)
)
}
SnackbarHost(hostState = state.snackbarHostState)
}
}

// Account-chooser dialog
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,13 @@ class ConversationsListViewModel @Inject constructor(

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

/** Tokens of rooms being left; hidden optimistically while the leave-undo snackbar is showing. */
private val pendingLeaveTokens = MutableStateFlow<Set<String>>(emptySet())

private val excludedRoomTokens = combine(hideRoomToken, pendingLeaveTokens) { hideToken, pendingTokens ->
if (hideToken != null) pendingTokens + hideToken else pendingTokens
}

private enum class SearchDisplayMode {
OFF,
ALL_CONVERSATIONS,
Expand Down Expand Up @@ -272,9 +279,9 @@ class ConversationsListViewModel @Inject constructor(
_filterStateFlow,
searchDisplayModeFlow,
combine(_selectedTagFilterFlow, selectedTagIsFavoritesFlow, ::TagFilterSelection),
combine(searchResultEntries, hideRoomToken, ::Pair)
) { rooms, filterState, searchMode, tagFilter, (searchResults, hideToken) ->
buildConversationListEntries(rooms, filterState, searchMode, tagFilter, searchResults, hideToken)
combine(searchResultEntries, excludedRoomTokens, ::Pair)
) { rooms, filterState, searchMode, tagFilter, (searchResults, excludedTokens) ->
buildConversationListEntries(rooms, filterState, searchMode, tagFilter, searchResults, excludedTokens)
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())

/**
Expand All @@ -288,9 +295,9 @@ class ConversationsListViewModel @Inject constructor(
getRoomsStateFlow,
_filterStateFlow,
searchDisplayModeFlow,
hideRoomToken
) { rooms, filterState, searchMode, hideToken ->
baseFilterRooms(rooms, filterState, searchMode, hideToken)
excludedRoomTokens
) { rooms, filterState, searchMode, excludedTokens ->
baseFilterRooms(rooms, filterState, searchMode, excludedTokens)
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())

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

/** Optimistically hide a room while its leave-undo snackbar is showing. */
fun markConversationPendingLeave(token: String) {
pendingLeaveTokens.value = pendingLeaveTokens.value + token
}

/** Un-hide a room, either because the leave was undone or because it finished/failed. */
fun clearConversationPendingLeave(token: String) {
pendingLeaveTokens.value = pendingLeaveTokens.value - token
}

fun getFederationInvitations() {
_federationInvitationHintVisible.value = false
_showAvatarBadge.value = false
Expand Down Expand Up @@ -654,11 +671,11 @@ class ConversationsListViewModel @Inject constructor(
searchMode: SearchDisplayMode,
tagFilter: TagFilterSelection,
searchResults: List<ConversationListEntry>,
hideToken: String?
excludedTokens: Set<String>
): List<ConversationListEntry> {
if (searchMode == SearchDisplayMode.RESULTS) return searchResults

var filtered = baseFilterRooms(rooms, filterState, searchMode, hideToken)
var filtered = baseFilterRooms(rooms, filterState, searchMode, excludedTokens)

if (searchMode != SearchDisplayMode.ALL_CONVERSATIONS) {
filtered = when {
Expand All @@ -684,14 +701,14 @@ class ConversationsListViewModel @Inject constructor(
rooms: List<ConversationModel>,
filterState: Map<String, Boolean>,
searchMode: SearchDisplayMode,
hideToken: String?
excludedTokens: Set<String>
): List<ConversationModel> {
val hasFilterEnabled = filterState[MENTION] == true ||
filterState[UNREAD] == true ||
filterState[ARCHIVE] == true

var filtered = rooms
.filter { it.token != hideToken }
.filter { it.token !in excludedTokens }
.filter { conversation ->
!(
conversation.objectType == ConversationEnums.ObjectType.ROOM &&
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ How to translate with transifex:

<!-- Conversation menu -->
<string name="nc_leave">Leave conversation</string>
<string name="nc_undo">Undo</string>
<string name="nc_clear_history">Delete all messages</string>
<string name="nc_clear_history_warning">Do you really want to delete all messages in this conversation?</string>
<string name="nc_clear_history_success">All messages were deleted</string>
Expand Down
Loading