From dd4fcffec4aa9c5fc6c9a20d7ba7c6f54a58f5c1 Mon Sep 17 00:00:00 2001 From: Michelle Tang Date: Mon, 21 Oct 2024 11:02:40 -0700 Subject: [PATCH] Fix various bugs for chat folders. --- .../securesms/avatar/AvatarImage.kt | 9 ++- .../chats/folders/ChatFolderContextMenu.kt | 79 ++++++++----------- .../app/chats/folders/ChatFoldersFragment.kt | 52 ++++++------ .../chats/folders/ChatFoldersRepository.kt | 4 +- .../app/chats/folders/ChatFoldersViewModel.kt | 2 +- .../chats/folders/CreateFoldersFragment.kt | 21 ++--- .../securesms/contacts/SelectedContacts.kt | 2 +- .../conversationlist/ChatFolderAdapter.kt | 14 ++-- .../ConversationListFragment.java | 69 ++++++++++++---- .../ConversationListViewModel.kt | 22 +----- .../securesms/database/ChatFolderTables.kt | 7 +- .../securesms/database/ThreadTable.kt | 21 +++++ .../main/MainActivityListHostFragment.kt | 10 +++ .../main/Material3OnScrollHelperBinder.kt | 1 + .../securesms/util/Material3OnScrollHelper.kt | 36 ++++++++- .../res/drawable/symbol_folder_settings.xml | 13 +++ .../res/layout/conversation_list_fragment.xml | 2 +- app/src/main/res/values/strings.xml | 8 +- 18 files changed, 228 insertions(+), 144 deletions(-) create mode 100644 app/src/main/res/drawable/symbol_folder_settings.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarImage.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarImage.kt index 6a78a607d4..bf83a5a773 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarImage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarImage.kt @@ -19,7 +19,8 @@ import org.thoughtcrime.securesms.recipients.Recipient @Composable fun AvatarImage( recipient: Recipient, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + useProfile: Boolean = true ) { if (LocalInspectionMode.current) { Spacer( @@ -31,7 +32,11 @@ fun AvatarImage( factory = ::AvatarImageView, modifier = modifier.background(color = Color.Transparent, shape = CircleShape) ) { - it.setAvatarUsingProfile(recipient) + if (useProfile) { + it.setAvatarUsingProfile(recipient) + } else { + it.setAvatar(recipient) + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFolderContextMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFolderContextMenu.kt index e80a1df2f4..9a2c816800 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFolderContextMenu.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFolderContextMenu.kt @@ -18,25 +18,27 @@ object ChatFolderContextMenu { anchorView: View, rootView: ViewGroup = anchorView.rootView as ViewGroup, folderType: ChatFolderRecord.FolderType, + unreadCount: Int, + isMuted: Boolean, onEdit: () -> Unit = {}, - onAdd: () -> Unit = {}, onMuteAll: () -> Unit = {}, + onUnmuteAll: () -> Unit = {}, onReadAll: () -> Unit = {}, - onDelete: () -> Unit = {}, - onReorder: () -> Unit = {} + onFolderSettings: () -> Unit = {} ) { show( context = context, anchorView = anchorView, rootView = rootView, folderType = folderType, + unreadCount = unreadCount, + isMuted = isMuted, callbacks = object : Callbacks { override fun onEdit() = onEdit() - override fun onAdd() = onAdd() override fun onMuteAll() = onMuteAll() + override fun onUnmuteAll() = onUnmuteAll() override fun onReadAll() = onReadAll() - override fun onDelete() = onDelete() - override fun onReorder() = onReorder() + override fun onFolderSettings() = onFolderSettings() } ) } @@ -45,29 +47,38 @@ object ChatFolderContextMenu { context: Context, anchorView: View, rootView: ViewGroup, + unreadCount: Int, + isMuted: Boolean, folderType: ChatFolderRecord.FolderType, callbacks: Callbacks ) { val actions = mutableListOf().apply { - if (folderType == ChatFolderRecord.FolderType.ALL) { - add( - ActionItem(R.drawable.symbol_plus_24, context.getString(R.string.ChatFoldersFragment__add_new_folder)) { - callbacks.onAdd() - } - ) - add( - ActionItem(R.drawable.symbol_bell_slash_24, context.getString(R.string.ChatFoldersFragment__mute_all)) { - callbacks.onMuteAll() - } - ) + if (unreadCount > 0) { add( ActionItem(R.drawable.symbol_chat_check, context.getString(R.string.ChatFoldersFragment__mark_all_read)) { callbacks.onReadAll() } ) + } + + if (isMuted) { add( - ActionItem(R.drawable.symbol_exchange_24, context.getString(R.string.ChatFoldersFragment__reorder_folder)) { - callbacks.onReorder() + ActionItem(R.drawable.symbol_bell_24, context.getString(R.string.ChatFoldersFragment__unmute_all)) { + callbacks.onUnmuteAll() + } + ) + } else { + add( + ActionItem(R.drawable.symbol_bell_slash_24, context.getString(R.string.ChatFoldersFragment__mute_all)) { + callbacks.onMuteAll() + } + ) + } + + if (folderType == ChatFolderRecord.FolderType.ALL) { + add( + ActionItem(R.drawable.symbol_folder_settings, context.getString(R.string.conversation_list_fragment__folder_settings)) { + callbacks.onFolderSettings() } ) } else { @@ -76,31 +87,6 @@ object ChatFolderContextMenu { callbacks.onEdit() } ) - add( - ActionItem(R.drawable.symbol_plus_24, context.getString(R.string.ChatFoldersFragment__add_new_folder)) { - callbacks.onAdd() - } - ) - add( - ActionItem(R.drawable.symbol_bell_slash_24, context.getString(R.string.ChatFoldersFragment__mute_all)) { - callbacks.onMuteAll() - } - ) - add( - ActionItem(R.drawable.symbol_chat_check, context.getString(R.string.ChatFoldersFragment__mark_all_read)) { - callbacks.onReadAll() - } - ) - add( - ActionItem(R.drawable.symbol_trash_24, context.getString(R.string.ChatFoldersFragment__delete_folder)) { - callbacks.onDelete() - } - ) - add( - ActionItem(R.drawable.symbol_exchange_24, context.getString(R.string.ChatFoldersFragment__reorder_folder)) { - callbacks.onReorder() - } - ) } } @@ -113,10 +99,9 @@ object ChatFolderContextMenu { private interface Callbacks { fun onEdit() - fun onAdd() fun onMuteAll() + fun onUnmuteAll() fun onReadAll() - fun onDelete() - fun onReorder() + fun onFolderSettings() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFoldersFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFoldersFragment.kt index b078176a6f..2f37fc07d9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFoldersFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFoldersFragment.kt @@ -104,24 +104,22 @@ fun FoldersScreen( } Column(modifier = modifier.verticalScroll(rememberScrollState())) { - Column(modifier = Modifier.padding(start = 24.dp)) { - Text( - text = stringResource(id = R.string.ChatFoldersFragment__organize_your_chats), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = 12.dp, bottom = 12.dp, end = 12.dp) - ) - Text( - text = stringResource(id = R.string.ChatFoldersFragment__folders), - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(top = 16.dp, bottom = 12.dp) - ) - FolderRow( - icon = R.drawable.symbol_plus_compact_16, - title = stringResource(R.string.ChatFoldersFragment__create_a_folder), - onClick = { onFolderClicked(ChatFolderRecord()) } - ) - } + Text( + text = stringResource(id = R.string.ChatFoldersFragment__organize_your_chats), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 12.dp, bottom = 12.dp, end = 12.dp, start = 24.dp) + ) + Text( + text = stringResource(id = R.string.ChatFoldersFragment__folders), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(top = 16.dp, bottom = 12.dp, start = 24.dp) + ) + FolderRow( + icon = R.drawable.symbol_plus_compact_16, + title = stringResource(R.string.ChatFoldersFragment__create_a_folder), + onClick = { onFolderClicked(ChatFolderRecord()) } + ) val columnHeight = dimensionResource(id = R.dimen.chat_folder_row_height).value * state.folders.size LazyColumn( @@ -142,8 +140,7 @@ fun FoldersScreen( { onFolderClicked(folder) } } else null, elevation = elevation, - showDragHandle = true, - modifier = Modifier.padding(start = 12.dp) + showDragHandle = true ) } } @@ -167,8 +164,7 @@ fun FoldersScreen( icon = R.drawable.symbol_chat_badge_24, title = title, subtitle = stringResource(R.string.ChatFoldersFragment__unread_messages), - onAdd = { onAdd(chatFolder) }, - modifier = Modifier.padding(start = 12.dp) + onAdd = { onAdd(chatFolder) } ) } ChatFolderRecord.FolderType.INDIVIDUAL -> { @@ -177,8 +173,7 @@ fun FoldersScreen( icon = R.drawable.symbol_person_light_24, title = title, subtitle = stringResource(R.string.ChatFoldersFragment__only_direct_messages), - onAdd = { onAdd(chatFolder) }, - modifier = Modifier.padding(start = 12.dp) + onAdd = { onAdd(chatFolder) } ) } ChatFolderRecord.FolderType.GROUP -> { @@ -187,8 +182,7 @@ fun FoldersScreen( icon = R.drawable.symbol_group_light_20, title = title, subtitle = stringResource(R.string.ChatFoldersFragment__only_group_messages), - onAdd = { onAdd(chatFolder) }, - modifier = Modifier.padding(start = 12.dp) + onAdd = { onAdd(chatFolder) } ) } ChatFolderRecord.FolderType.ALL -> { @@ -239,17 +233,19 @@ fun FolderRow( verticalAlignment = Alignment.CenterVertically, modifier = if (onClick != null) { modifier - .padding(end = 12.dp) .clickable(onClick = onClick) .fillMaxWidth() .defaultMinSize(minHeight = dimensionResource(id = R.dimen.chat_folder_row_height)) .shadow(elevation = elevation) + .background(MaterialTheme.colorScheme.background) + .padding(start = 24.dp, end = 12.dp) } else { modifier - .padding(end = 12.dp) .fillMaxWidth() .defaultMinSize(minHeight = dimensionResource(id = R.dimen.chat_folder_row_height)) .shadow(elevation = elevation) + .background(MaterialTheme.colorScheme.background) + .padding(start = 24.dp, end = 12.dp) } ) { Image( diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFoldersRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFoldersRepository.kt index c8fb037850..ed4164bc98 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFoldersRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFoldersRepository.kt @@ -7,8 +7,8 @@ import org.thoughtcrime.securesms.database.SignalDatabase */ object ChatFoldersRepository { - fun getCurrentFolders(includeUnreadCount: Boolean = false): List { - return SignalDatabase.chatFolders.getChatFolders(includeUnreadCount) + fun getCurrentFolders(includeUnreadAndMutedCounts: Boolean = false): List { + return SignalDatabase.chatFolders.getChatFolders(includeUnreadAndMutedCounts) } fun createFolder(folder: ChatFolderRecord) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFoldersViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFoldersViewModel.kt index 38ee00afba..6445ffe659 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFoldersViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFoldersViewModel.kt @@ -24,7 +24,7 @@ class ChatFoldersViewModel : ViewModel() { fun loadCurrentFolders(context: Context) { viewModelScope.launch(Dispatchers.IO) { - val folders = ChatFoldersRepository.getCurrentFolders(includeUnreadCount = false) + val folders = ChatFoldersRepository.getCurrentFolders(includeUnreadAndMutedCounts = false) val suggestedFolders = getSuggestedFolders(context, folders) internalState.update { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/CreateFoldersFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/CreateFoldersFragment.kt index 1b907ac69b..0955119d59 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/CreateFoldersFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/CreateFoldersFragment.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Person +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch @@ -229,16 +230,14 @@ fun CreateFolderScreen( FolderRow( icon = R.drawable.symbol_plus_compact_16, title = stringResource(R.string.CreateFoldersFragment__add_chats), - onClick = onAddChat, - modifier = Modifier.padding(start = 12.dp) + onClick = onAddChat ) if (state.currentFolder.showIndividualChats) { FolderRow( icon = R.drawable.symbol_person_light_24, title = stringResource(R.string.ChatFoldersFragment__one_on_one_chats), - onClick = onAddChat, - modifier = Modifier.padding(start = 12.dp) + onClick = onAddChat ) } @@ -246,8 +245,7 @@ fun CreateFolderScreen( FolderRow( icon = R.drawable.symbol_group_light_20, title = stringResource(R.string.ChatFoldersFragment__groups), - onClick = onAddChat, - modifier = Modifier.padding(start = 12.dp) + onClick = onAddChat ) } } @@ -277,8 +275,7 @@ fun CreateFolderScreen( FolderRow( icon = R.drawable.symbol_plus_compact_16, title = stringResource(R.string.CreateFoldersFragment__exclude_chats), - onClick = onRemoveChat, - modifier = Modifier.padding(start = 12.dp) + onClick = onRemoveChat ) } @@ -334,6 +331,9 @@ fun CreateFolderScreen( } } else if (!isNewFolder) { Buttons.MediumTonal( + colors = ButtonDefaults.filledTonalButtonColors( + disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant + ), enabled = hasChanges, onClick = { onCreateConfirmed(true) }, modifier = modifier @@ -451,10 +451,11 @@ fun ChatRow( recipient = recipient, modifier = Modifier .padding(start = 24.dp, end = 16.dp) - .size(40.dp) + .size(40.dp), + useProfile = false ) } - Text(text = recipient.getShortDisplayName(LocalContext.current)) + Text(text = if (recipient.isSelf) stringResource(id = R.string.note_to_self) else recipient.getShortDisplayName(LocalContext.current)) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectedContacts.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectedContacts.kt index 85024b5257..3cfebd85e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectedContacts.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectedContacts.kt @@ -37,7 +37,7 @@ object SelectedContacts { private val chip: ContactChip = itemView.findViewById(R.id.contact_chip) override fun bind(model: RecipientModel) { - chip.text = model.recipient.getShortDisplayName(context) + chip.text = if (model.recipient.isSelf) context.getString(R.string.note_to_self) else model.recipient.getShortDisplayName(context) chip.setContact(model.selectedContact) chip.isCloseIconVisible = true chip.setOnCloseIconClickListener { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ChatFolderAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ChatFolderAdapter.kt index 8130b648c7..e78b99ee21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ChatFolderAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ChatFolderAdapter.kt @@ -42,17 +42,18 @@ class ChatFolderAdapter(val callbacks: Callbacks) : MappingAdapter() { context = itemView.context, anchorView = view, folderType = model.chatFolder.folderType, + unreadCount = folder.unreadCount, + isMuted = folder.isMuted, onEdit = { callbacks.onEdit(model.chatFolder) }, - onAdd = { callbacks.onAdd() }, onMuteAll = { callbacks.onMuteAll(model.chatFolder) }, + onUnmuteAll = { callbacks.onUnmuteAll(model.chatFolder) }, onReadAll = { callbacks.onReadAll(model.chatFolder) }, - onDelete = { callbacks.onDelete(model.chatFolder) }, - onReorder = { callbacks.onReorder() } + onFolderSettings = { callbacks.onFolderSettings() } ) true } if (model.isSelected) { - itemView.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(itemView.context, R.color.signal_colorSurfaceVariant)) + itemView.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(itemView.context, R.color.signal_colorSurface2)) } else { itemView.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(itemView.context, R.color.transparent)) } @@ -70,10 +71,9 @@ class ChatFolderAdapter(val callbacks: Callbacks) : MappingAdapter() { interface Callbacks { fun onChatFolderClicked(chatFolder: ChatFolderRecord) fun onEdit(chatFolder: ChatFolderRecord) - fun onAdd() fun onMuteAll(chatFolder: ChatFolderRecord) + fun onUnmuteAll(chatFolder: ChatFolderRecord) fun onReadAll(chatFolder: ChatFolderRecord) - fun onDelete(chatFolder: ChatFolderRecord) - fun onReorder() + fun onFolderSettings() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index e21214edfe..1cbe841508 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -21,6 +21,7 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; @@ -64,6 +65,7 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.LinearSmoothScroller; import androidx.recyclerview.widget.RecyclerView; import com.airbnb.lottie.SimpleColorFilter; @@ -243,6 +245,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode private SignalContextMenu activeContextMenu; private LifecycleDisposable lifecycleDisposable; private ChatFolderAdapter chatFolderAdapter; + private RecyclerView.SmoothScroller smoothScroller; protected ConversationListArchiveItemDecoration archiveDecoration; protected ConversationListItemAnimator itemAnimator; @@ -458,7 +461,22 @@ public class ConversationListFragment extends MainFragment implements ActionMode } })); - requireCallback().bindScrollHelper(list); + requireCallback().bindScrollHelper(list, chatFolderList, color -> { + for (int i = 0; i < chatFolderList.getChildCount(); i++) { + View child = chatFolderList.getChildAt(i); + if (child != null && child.isSelected()) { + child.setBackgroundTintList(ColorStateList.valueOf(color)); + } + } + return Unit.INSTANCE; + }); + + smoothScroller = new LinearSmoothScroller(requireContext()) { + @Override + protected int calculateTimeForScrolling(int dx) { + return 150; + } + }; } @Override @@ -1048,6 +1066,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode } private void onChatFoldersChanged(List folders) { + chatFolderList.setVisibility(folders.size() > 1 ? View.VISIBLE : View.GONE); chatFolderAdapter.submitList(new ArrayList<>(folders)); } @@ -1673,7 +1692,35 @@ public class ConversationListFragment extends MainFragment implements ActionMode @Override public void onChatFolderClicked(@NonNull ChatFolderRecord chatFolder) { + int oldIndex = -1; + int newIndex = -1; + + for (int i = 0; i < viewModel.getFolders().size(); i++) { + if (oldIndex != -1 && newIndex != -1) { + break; + } + + ChatFolderMappingModel folder = viewModel.getFolders().get(i); + if (folder.isSelected()) { + oldIndex = i; + } + if (folder.getChatFolder().getId() == chatFolder.getId()) { + newIndex = i; + } + } + + if (oldIndex < newIndex) { + smoothScroller.setTargetPosition(Math.min(newIndex + 1, viewModel.getFolders().size())); + } else { + smoothScroller.setTargetPosition(Math.max(newIndex - 1, 0)); + } + + if (chatFolderList.getLayoutManager() != null) { + chatFolderList.getLayoutManager().startSmoothScroll(smoothScroller); + } + viewModel.select(chatFolder); + list.smoothScrollToPosition(0); } @Override @@ -1682,13 +1729,13 @@ public class ConversationListFragment extends MainFragment implements ActionMode } @Override - public void onAdd() { - startActivity(AppSettingsActivity.createChatFolder(requireContext(), -1)); + public void onMuteAll(@NonNull ChatFolderRecord chatFolder) { + MuteDialog.show(requireContext(), until -> viewModel.onUpdateMute(chatFolder, until)); } @Override - public void onMuteAll(@NonNull ChatFolderRecord chatFolder) { - MuteDialog.show(requireContext(), until -> viewModel.onMuteChatFolder(chatFolder, until)); + public void onUnmuteAll(@NonNull ChatFolderRecord chatFolder) { + viewModel.onUpdateMute(chatFolder, 0); } @Override @@ -1701,16 +1748,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode } @Override - public void onDelete(@NonNull ChatFolderRecord chatFolder) { - new MaterialAlertDialogBuilder(requireActivity()) - .setMessage(getString(R.string.CreateFoldersFragment__delete_this_chat_folder)) - .setPositiveButton(R.string.delete, (dialog, which) -> viewModel.deleteChatFolder(chatFolder)) - .setNegativeButton(android.R.string.cancel, null) - .show(); - } - - @Override - public void onReorder() { + public void onFolderSettings() { startActivity(AppSettingsActivity.chatFolders(requireContext())); } @@ -1763,6 +1801,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode if (viewHolder.itemView instanceof ConversationListItemAction || viewHolder instanceof ConversationListAdapter.HeaderViewHolder || viewHolder instanceof ClearFilterViewHolder || + viewHolder instanceof ConversationListAdapter.EmptyFolderViewHolder || actionMode != null || viewHolder.itemView.isSelected() || activeAdapter == searchAdapter) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.kt index 040c02ae1c..7ef1d431c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.kt @@ -8,7 +8,6 @@ import io.reactivex.rxjava3.core.BackpressureStrategy import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.Flowables import io.reactivex.rxjava3.kotlin.addTo import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.Dispatchers @@ -88,6 +87,7 @@ class ConversationListViewModel( conversationListDataSource = store .stateFlowable .subscribeOn(Schedulers.io()) + .filter { it.currentFolder.id != -1L } .map { it.filterRequest to it.currentFolder } .distinctUntilChanged() .map { (filterRequest, folder) -> @@ -115,7 +115,7 @@ class ConversationListViewModel( .subscribe { controller.onDataInvalidated() } .addTo(disposables) - Flowables.combineLatest( + Flowable.merge( RxDatabaseObserver .conversationList .debounce(250, TimeUnit.MILLISECONDS), @@ -219,7 +219,7 @@ class ConversationListViewModel( private fun loadCurrentFolders() { viewModelScope.launch(Dispatchers.IO) { - val folders = ChatFoldersRepository.getCurrentFolders(includeUnreadCount = true) + val folders = ChatFoldersRepository.getCurrentFolders(includeUnreadAndMutedCounts = true) val selectedFolderId = if (currentFolder.id == -1L) { folders.firstOrNull()?.id @@ -262,7 +262,7 @@ class ConversationListViewModel( } } - fun onMuteChatFolder(chatFolder: ChatFolderRecord, until: Long) { + fun onUpdateMute(chatFolder: ChatFolderRecord, until: Long) { viewModelScope.launch(Dispatchers.IO) { val ids = SignalDatabase.threads.getRecipientIdsByChatFolder(chatFolder) val recipientIds: List = ids.filter { id -> @@ -274,20 +274,6 @@ class ConversationListViewModel( } } - fun deleteChatFolder(chatFolder: ChatFolderRecord) { - viewModelScope.launch(Dispatchers.IO) { - SignalDatabase.chatFolders.deleteChatFolder(chatFolder) - val updatedFolders = folders.filter { folder -> folder.chatFolder.id != chatFolder.id } - - store.update { - it.copy( - currentFolder = updatedFolders.first().chatFolder, - chatFolders = updatedFolders - ) - } - } - } - fun markChatFolderRead(chatFolder: ChatFolderRecord) { viewModelScope.launch(Dispatchers.IO) { val ids = SignalDatabase.threads.getThreadIdsByChatFolder(chatFolder) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ChatFolderTables.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ChatFolderTables.kt index d9a2ac8b7f..4750b81ce6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ChatFolderTables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ChatFolderTables.kt @@ -152,7 +152,7 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat /** * Maps the chat folder ids to its corresponding chat folder */ - fun getChatFolders(includeUnreads: Boolean = false): List { + fun getChatFolders(includeUnreadAndMutedCount: Boolean = false): List { val includedChats: Map> = getIncludedChats() val excludedChats: Map> = getExcludedChats() @@ -178,10 +178,11 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat ) } - if (includeUnreads) { + if (includeUnreadAndMutedCount) { return folders.map { folder -> folder.copy( - unreadCount = SignalDatabase.threads.getUnreadCountByChatFolder(folder) + unreadCount = SignalDatabase.threads.getUnreadCountByChatFolder(folder), + isMuted = !SignalDatabase.threads.hasUnmutedChatsInFolder(folder) ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt index add4951c75..1fef9dd423 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt @@ -18,6 +18,7 @@ import org.signal.core.util.exists import org.signal.core.util.logging.Log import org.signal.core.util.or import org.signal.core.util.readToList +import org.signal.core.util.readToSingleBoolean import org.signal.core.util.readToSingleInt import org.signal.core.util.readToSingleLong import org.signal.core.util.requireBoolean @@ -631,6 +632,26 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa return allCount + forcedUnreadCount } + /** + * Returns whether or not there are any unmuted chats in a chat folder + */ + fun hasUnmutedChatsInFolder(folder: ChatFolderRecord): Boolean { + val chatFolderQuery = folder.toQuery() + + val unmutedChats = + """ + SELECT COUNT(${RecipientTable.MUTE_UNTIL}) + FROM $TABLE_NAME + LEFT OUTER JOIN ${RecipientTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID} + WHERE + $ARCHIVED = 0 AND + ${RecipientTable.MUTE_UNTIL} = 0 + $chatFolderQuery + """ + + return readableDatabase.rawQuery(unmutedChats, null).readToSingleBoolean() + } + /** * Returns the number of unread messages across all threads within a chat folder * Threads that are forced-unread count as 1. diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/MainActivityListHostFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/main/MainActivityListHostFragment.kt index c38b67562a..be58c02c76 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/main/MainActivityListHostFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/main/MainActivityListHostFragment.kt @@ -386,4 +386,14 @@ class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_f viewLifecycleOwner ).attach(recyclerView) } + + override fun bindScrollHelper(recyclerView: RecyclerView, chatFolders: RecyclerView, setChatFolder: (Int) -> Unit) { + Material3OnScrollHelper( + activity = requireActivity(), + views = listOf(_toolbarBackground, chatFolders), + viewStubs = listOf(_searchToolbar), + lifecycleOwner = viewLifecycleOwner, + setChatFolderColor = setChatFolder + ).attach(recyclerView) + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/Material3OnScrollHelperBinder.kt b/app/src/main/java/org/thoughtcrime/securesms/main/Material3OnScrollHelperBinder.kt index 23f313abc9..468ff24f1b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/main/Material3OnScrollHelperBinder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/main/Material3OnScrollHelperBinder.kt @@ -4,4 +4,5 @@ import androidx.recyclerview.widget.RecyclerView interface Material3OnScrollHelperBinder { fun bindScrollHelper(recyclerView: RecyclerView) + fun bindScrollHelper(recyclerView: RecyclerView, chatFolders: RecyclerView, setChatFolder: (Int) -> Unit) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Material3OnScrollHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/util/Material3OnScrollHelper.kt index f8499e4b47..3f8da83453 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Material3OnScrollHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Material3OnScrollHelper.kt @@ -25,6 +25,7 @@ open class Material3OnScrollHelper( private val context: Context, private val setStatusBarColor: (Int) -> Unit, private val getStatusBarColor: () -> Int, + private val setChatFolderColor: (Int) -> Unit = {}, private val views: List, private val viewStubs: List> = emptyList(), lifecycleOwner: LifecycleOwner @@ -33,16 +34,39 @@ open class Material3OnScrollHelper( constructor(activity: Activity, view: View, lifecycleOwner: LifecycleOwner) : this(activity = activity, views = listOf(view), lifecycleOwner = lifecycleOwner) constructor(activity: Activity, views: List, viewStubs: List> = emptyList(), lifecycleOwner: LifecycleOwner) : this( + activity = activity, + views = views, + viewStubs = viewStubs, + lifecycleOwner = lifecycleOwner, + setChatFolderColor = {} + ) + + constructor( + activity: Activity, + views: List, + viewStubs: List> = emptyList(), + lifecycleOwner: LifecycleOwner, + setChatFolderColor: (Int) -> Unit = {} + ) : this( context = activity, setStatusBarColor = { WindowUtil.setStatusBarColor(activity.window, it) }, getStatusBarColor = { WindowUtil.getStatusBarColor(activity.window) }, + setChatFolderColor = setChatFolderColor, views = views, viewStubs = viewStubs, lifecycleOwner = lifecycleOwner ) - open val activeColorSet: ColorSet = ColorSet(R.color.signal_colorSurface2) - open val inactiveColorSet: ColorSet = ColorSet(R.color.signal_colorBackground) + open val activeColorSet: ColorSet = ColorSet( + toolbarColorRes = R.color.signal_colorSurface2, + statusBarColorRes = R.color.signal_colorSurface2, + chatFolderColorRes = R.color.signal_colorBackground + ) + open val inactiveColorSet: ColorSet = ColorSet( + toolbarColorRes = R.color.signal_colorBackground, + statusBarColorRes = R.color.signal_colorBackground, + chatFolderColorRes = R.color.signal_colorSurface2 + ) protected var previousStatusBarColor: Int = getStatusBarColor() @@ -94,6 +118,7 @@ open class Material3OnScrollHelper( val colorSet = if (active == true) activeColorSet else inactiveColorSet setToolbarColor(ContextCompat.getColor(context, colorSet.toolbarColorRes)) setStatusBarColor(ContextCompat.getColor(context, colorSet.statusBarColorRes)) + setChatFolderColor(ContextCompat.getColor(context, colorSet.chatFolderColorRes)) } private fun updateActiveState(isActive: Boolean) { @@ -118,12 +143,15 @@ open class Material3OnScrollHelper( val endToolbarColor = ContextCompat.getColor(context, endColorSet.toolbarColorRes) val startStatusBarColor = ContextCompat.getColor(context, startColorSet.statusBarColorRes) val endStatusBarColor = ContextCompat.getColor(context, endColorSet.statusBarColorRes) + val startChatFolderColor = ContextCompat.getColor(context, startColorSet.chatFolderColorRes) + val endChatFolderColor = ContextCompat.getColor(context, endColorSet.chatFolderColorRes) animator = ValueAnimator.ofFloat(0f, 1f).apply { duration = 200 addUpdateListener { setToolbarColor(ArgbEvaluatorCompat.getInstance().evaluate(it.animatedFraction, startToolbarColor, endToolbarColor)) setStatusBarColor(ArgbEvaluatorCompat.getInstance().evaluate(it.animatedFraction, startStatusBarColor, endStatusBarColor)) + setChatFolderColor(ArgbEvaluatorCompat.getInstance().evaluate(it.animatedFraction, startChatFolderColor, endChatFolderColor)) } start() } @@ -157,8 +185,10 @@ open class Material3OnScrollHelper( */ data class ColorSet( @ColorRes val toolbarColorRes: Int, - @ColorRes val statusBarColorRes: Int + @ColorRes val statusBarColorRes: Int, + @ColorRes val chatFolderColorRes: Int ) { constructor(@ColorRes color: Int) : this(color, color) + constructor(@ColorRes toolbarColorRes: Int, @ColorRes statusBarColorRes: Int) : this(toolbarColorRes, statusBarColorRes, toolbarColorRes) } } diff --git a/app/src/main/res/drawable/symbol_folder_settings.xml b/app/src/main/res/drawable/symbol_folder_settings.xml new file mode 100644 index 0000000000..8ac91d2937 --- /dev/null +++ b/app/src/main/res/drawable/symbol_folder_settings.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/layout/conversation_list_fragment.xml b/app/src/main/res/layout/conversation_list_fragment.xml index 673eaf3cac..ed589763ab 100644 --- a/app/src/main/res/layout/conversation_list_fragment.xml +++ b/app/src/main/res/layout/conversation_list_fragment.xml @@ -34,7 +34,7 @@ %1$s folder added. Edit folder - - Delete folder - - Add new folder Mute all - - Reorder folders + + Unmute all Mark all read