Fix issue where archival progress dialog never dismisses.

This commit is contained in:
Alex Hart
2026-01-21 12:18:57 -04:00
parent 791a38a181
commit d16be8c4d7
5 changed files with 67 additions and 26 deletions

View File

@@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.R
*
* @property message The text message to display in the snackbar.
* @property actionState Optional action button configuration.
* @property showProgress Whether to show a progress indicator in the snackbar.
* @property duration How long the snackbar should be displayed.
* @property hostKey The target host where this snackbar should be displayed. Defaults to [SnackbarHostKey.Global]
* @property fallbackKey Optional host to fallback upon if the host key is not registered. Defaults to the Global key.
@@ -22,7 +21,6 @@ import org.thoughtcrime.securesms.R
data class SnackbarState(
val message: String,
val actionState: ActionState? = null,
val showProgress: Boolean = false,
val duration: Snackbars.Duration = Snackbars.Duration.SHORT,
val hostKey: SnackbarHostKey = SnackbarHostKey.Global,
val fallbackKey: SnackbarHostKey? = SnackbarHostKey.Global

View File

@@ -8,7 +8,7 @@ package org.thoughtcrime.securesms.components.snackbars
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
@@ -27,11 +27,30 @@ val LocalSnackbarStateConsumerRegistry = staticCompositionLocalOf<SnackbarStateC
error("No SnackbarStateConsumerRegistry provided")
}
/**
* Holder for snackbar state that allows clearing the state after consumption.
*/
@Stable
class SnackbarStateHolder(
private val state: MutableState<SnackbarState?>
) {
val value: SnackbarState?
get() = state.value
/**
* Clears the current snackbar state. Should be called after the snackbar has been consumed/dismissed.
*/
fun clear() {
state.value = null
}
}
@Composable
fun rememberSnackbarState(
key: SnackbarHostKey
): State<SnackbarState?> {
): SnackbarStateHolder {
val state: MutableState<SnackbarState?> = remember(key) { mutableStateOf(null) }
val holder = remember(key, state) { SnackbarStateHolder(state) }
val registry = LocalSnackbarStateConsumerRegistry.current
DisposableEffect(registry, key) {
@@ -44,7 +63,7 @@ fun rememberSnackbarState(
}
}
return state
return holder
}
/**

View File

@@ -151,7 +151,6 @@ public class ConversationListArchiveFragment extends ConversationListFragment
return Unit.INSTANCE;
}
),
false,
Snackbars.Duration.LONG,
MainSnackbarHostKey.MainChrome.INSTANCE,
null

View File

@@ -68,6 +68,7 @@ import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.DimensionUnit;
import org.signal.core.util.Stopwatch;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.concurrent.SimpleTask;
@@ -158,7 +159,7 @@ import org.thoughtcrime.securesms.util.SignalProxyUtil;
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.thoughtcrime.securesms.components.SignalProgressDialog;
import org.thoughtcrime.securesms.util.views.Stub;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
@@ -230,6 +231,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
private ChatListBackHandler chatListBackHandler;
private BannerManager bannerManager;
private SignalProgressDialog progressDialog;
protected MainNavigationViewModel mainNavigationViewModel;
@@ -464,6 +466,8 @@ public class ConversationListFragment extends MainFragment implements Conversati
defaultAdapter = null;
searchAdapter = null;
dismissProgressDialog();
super.onDestroyView();
}
@@ -968,16 +972,23 @@ public class ConversationListFragment extends MainFragment implements Conversati
}
@SuppressLint("StaticFieldLeak")
private void handleArchive(@NonNull Collection<Long> ids, boolean showProgress) {
private void handleArchive(@NonNull Collection<Long> ids) {
Set<Long> selectedConversations = new HashSet<>(ids);
int count = selectedConversations.size();
String snackBarTitle = getResources().getQuantityString(getArchivedSnackbarTitleRes(), count, count);
boolean showProgress = count > 1;
dismissProgressDialog();
if (showProgress) {
progressDialog = SignalProgressDialog.show(requireContext(), null, null, true, false, null);
}
lifecycleDisposable.add(Completable
.fromAction(() -> archiveThreads(selectedConversations))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
dismissProgressDialog();
endActionModeIfActive();
mainNavigationViewModel.getSnackbarRegistry().emit(new SnackbarState(
@@ -986,11 +997,10 @@ public class ConversationListFragment extends MainFragment implements Conversati
getString(R.string.ConversationListFragment_undo),
R.color.amber_500,
() -> {
SignalExecutors.BOUNDED_IO.execute(() -> reverseArchiveThreads(selectedConversations));
handleUnarchive(selectedConversations);
return Unit.INSTANCE;
}
),
showProgress,
Snackbars.Duration.LONG,
MainSnackbarHostKey.MainChrome.INSTANCE,
null
@@ -998,6 +1008,27 @@ public class ConversationListFragment extends MainFragment implements Conversati
}));
}
private void handleUnarchive(@NonNull Set<Long> threadIds) {
boolean showProgress = threadIds.size() > 1;
dismissProgressDialog();
if (showProgress) {
progressDialog = SignalProgressDialog.show(requireContext(), null, null, true, false, null);
}
SignalExecutors.BOUNDED_IO.execute(() -> {
reverseArchiveThreads(threadIds);
ThreadUtil.runOnMain(this::dismissProgressDialog);
});
}
private void dismissProgressDialog() {
if (progressDialog != null) {
progressDialog.dismiss();
progressDialog = null;
}
}
@SuppressLint("StaticFieldLeak")
private void handleDelete(@NonNull Collection<Long> ids) {
if (DeleteSyncEducationDialog.shouldShow()) {
@@ -1074,7 +1105,6 @@ public class ConversationListFragment extends MainFragment implements Conversati
mainNavigationViewModel.getSnackbarRegistry().emit(new SnackbarState(
getString(R.string.conversation_list__you_can_only_pin_up_to_d_chats, MAXIMUM_PINNED_CONVERSATIONS),
null,
false,
Snackbars.Duration.LONG,
MainSnackbarHostKey.MainChrome.INSTANCE,
null
@@ -1120,7 +1150,8 @@ public class ConversationListFragment extends MainFragment implements Conversati
}
private void updateMute(@NonNull Collection<Conversation> conversations, long until) {
SimpleProgressDialog.DismissibleDialog dialog = SimpleProgressDialog.showDelayed(requireContext(), 250, 250);
dismissProgressDialog();
progressDialog = SignalProgressDialog.show(requireContext(), null, null, true, false, null);
SimpleTask.run(SignalExecutors.BOUNDED, () -> {
List<RecipientId> recipientIds = conversations.stream()
@@ -1132,7 +1163,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
return null;
}, unused -> {
endActionModeIfActive();
dialog.dismiss();
dismissProgressDialog();
});
}
@@ -1245,7 +1276,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
}));
if (conversation.getThreadRecord().isArchived()) {
items.add(new ActionItem(R.drawable.symbol_archive_up_24, getResources().getString(R.string.ConversationListFragment_unarchive), () -> handleArchive(id, false)));
items.add(new ActionItem(R.drawable.symbol_archive_up_24, getResources().getString(R.string.ConversationListFragment_unarchive), () -> handleArchive(id)));
} else {
if (viewModel.getCurrentFolder().getFolderType() == ChatFolderRecord.FolderType.ALL &&
(conversation.getThreadRecord().getRecipient().isIndividual() ||
@@ -1257,7 +1288,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
} else if (viewModel.getCurrentFolder().getFolderType() != ChatFolderRecord.FolderType.ALL) {
items.add(new ActionItem(R.drawable.symbol_folder_minus, getString(R.string.ConversationListFragment_remove_from_folder), () -> viewModel.removeChatFromFolder(conversation.getThreadRecord().getThreadId())));
}
items.add(new ActionItem(R.drawable.symbol_archive_24, getResources().getString(R.string.ConversationListFragment_archive), () -> handleArchive(id, false)));
items.add(new ActionItem(R.drawable.symbol_archive_24, getResources().getString(R.string.ConversationListFragment_archive), () -> handleArchive(id)));
}
items.add(new ActionItem(R.drawable.symbol_trash_24, getResources().getString(R.string.ConversationListFragment_delete), () -> handleDelete(id)));
@@ -1360,9 +1391,9 @@ public class ConversationListFragment extends MainFragment implements Conversati
}
if (isArchived()) {
items.add(new ActionItem(R.drawable.symbol_archive_up_24, getResources().getString(R.string.ConversationListFragment_unarchive), () -> handleArchive(selectionIds, true)));
items.add(new ActionItem(R.drawable.symbol_archive_up_24, getResources().getString(R.string.ConversationListFragment_unarchive), () -> handleArchive(selectionIds)));
} else {
items.add(new ActionItem(R.drawable.symbol_archive_24, getResources().getString(R.string.ConversationListFragment_archive), () -> handleArchive(selectionIds, true)));
items.add(new ActionItem(R.drawable.symbol_archive_24, getResources().getString(R.string.ConversationListFragment_archive), () -> handleArchive(selectionIds)));
}
items.add(new ActionItem(R.drawable.symbol_trash_24, getResources().getString(R.string.ConversationListFragment_delete), () -> handleDelete(selectionIds)));
@@ -1440,7 +1471,6 @@ public class ConversationListFragment extends MainFragment implements Conversati
return Unit.INSTANCE;
}
),
false,
Snackbars.Duration.LONG,
MainSnackbarHostKey.MainChrome.INSTANCE,
null

View File

@@ -21,12 +21,10 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Snackbars
import org.signal.core.ui.compose.showSnackbar
import org.thoughtcrime.securesms.components.snackbars.SnackbarHostKey
import org.thoughtcrime.securesms.components.snackbars.SnackbarState
import org.thoughtcrime.securesms.components.snackbars.rememberSnackbarState
import org.thoughtcrime.securesms.megaphone.Megaphone
import org.thoughtcrime.securesms.megaphone.MegaphoneActionController
@@ -118,18 +116,14 @@ fun MainSnackbar(
hostKey: SnackbarHostKey = MainSnackbarHostKey.MainChrome
) {
val hostState = remember { SnackbarHostState() }
val state: SnackbarState? by rememberSnackbarState(hostKey)
val snackbarState = state
val stateHolder = rememberSnackbarState(hostKey)
val snackbarState = stateHolder.value
Snackbars.Host(
hostState,
modifier = modifier
)
if (snackbarState?.showProgress == true) {
Dialogs.IndeterminateProgressDialog()
}
LaunchedEffect(snackbarState) {
if (snackbarState != null) {
val result = hostState.showSnackbar(
@@ -143,6 +137,7 @@ fun MainSnackbar(
SnackbarResult.ActionPerformed -> snackbarState.actionState?.onActionClick?.invoke()
}
stateHolder.clear()
onDismissed()
}
}