Fix various bugs for chat folders.

This commit is contained in:
Michelle Tang 2024-10-21 11:02:40 -07:00 committed by Greyson Parrelli
parent b519bf6772
commit dd4fcffec4
18 changed files with 228 additions and 144 deletions

View file

@ -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)
}
}
}
}

View file

@ -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<ActionItem>().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()
}
}

View file

@ -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(

View file

@ -7,8 +7,8 @@ import org.thoughtcrime.securesms.database.SignalDatabase
*/
object ChatFoldersRepository {
fun getCurrentFolders(includeUnreadCount: Boolean = false): List<ChatFolderRecord> {
return SignalDatabase.chatFolders.getChatFolders(includeUnreadCount)
fun getCurrentFolders(includeUnreadAndMutedCounts: Boolean = false): List<ChatFolderRecord> {
return SignalDatabase.chatFolders.getChatFolders(includeUnreadAndMutedCounts)
}
fun createFolder(folder: ChatFolderRecord) {

View file

@ -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 {

View file

@ -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))
}
}

View file

@ -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 {

View file

@ -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()
}
}

View file

@ -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<ChatFolderMappingModel> 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)

View file

@ -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<RecipientId> = 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)

View file

@ -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<ChatFolderRecord> {
fun getChatFolders(includeUnreadAndMutedCount: Boolean = false): List<ChatFolderRecord> {
val includedChats: Map<Long, List<Long>> = getIncludedChats()
val excludedChats: Map<Long, List<Long>> = 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)
)
}
}

View file

@ -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.

View file

@ -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)
}
}

View file

@ -4,4 +4,5 @@ import androidx.recyclerview.widget.RecyclerView
interface Material3OnScrollHelperBinder {
fun bindScrollHelper(recyclerView: RecyclerView)
fun bindScrollHelper(recyclerView: RecyclerView, chatFolders: RecyclerView, setChatFolder: (Int) -> Unit)
}

View file

@ -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<View>,
private val viewStubs: List<Stub<out View>> = 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<View>, viewStubs: List<Stub<out View>> = emptyList(), lifecycleOwner: LifecycleOwner) : this(
activity = activity,
views = views,
viewStubs = viewStubs,
lifecycleOwner = lifecycleOwner,
setChatFolderColor = {}
)
constructor(
activity: Activity,
views: List<View>,
viewStubs: List<Stub<out View>> = 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)
}
}

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12.774,4.125L10.989,4.125C10.576,4.125 10.178,3.968 9.877,3.685L9.72,3.538C9.094,2.951 8.269,2.625 7.411,2.625H5C3.136,2.625 1.625,4.136 1.625,6V13.202V13.263V15.237V15.237C1.625,16.046 1.625,16.706 1.669,17.242C1.714,17.796 1.811,18.294 2.047,18.759C2.419,19.488 3.012,20.081 3.741,20.453C4.206,20.69 4.704,20.786 5.258,20.831C5.794,20.875 6.454,20.875 7.263,20.875H16.737C17.546,20.875 18.206,20.875 18.742,20.831C19.296,20.786 19.794,20.69 20.259,20.453C20.988,20.081 21.581,19.488 21.953,18.759C22.19,18.294 22.286,17.796 22.331,17.242C22.375,16.706 22.375,16.045 22.375,15.237L22.375,11.556C21.837,11.884 21.247,12.135 20.621,12.296C20.625,12.585 20.625,12.916 20.625,13.3V15.2C20.625,16.055 20.624,16.643 20.587,17.099C20.551,17.545 20.484,17.788 20.393,17.965C20.19,18.365 19.865,18.69 19.465,18.893C19.288,18.984 19.045,19.051 18.599,19.087C18.143,19.124 17.555,19.125 16.7,19.125H7.3C6.445,19.125 5.857,19.124 5.401,19.087C4.955,19.051 4.712,18.984 4.535,18.893C4.135,18.69 3.81,18.365 3.607,17.965C3.516,17.788 3.449,17.545 3.413,17.099C3.376,16.643 3.375,16.055 3.375,15.2V13.3C3.375,12.446 3.376,11.857 3.413,11.401C3.449,10.955 3.516,10.712 3.607,10.535C3.81,10.135 4.135,9.81 4.535,9.607C4.712,9.516 4.955,9.449 5.401,9.413C5.857,9.376 6.445,9.375 7.3,9.375H13.444C13.117,8.838 12.866,8.25 12.705,7.625H7.263C6.454,7.625 5.794,7.625 5.258,7.669C4.704,7.714 4.206,7.811 3.741,8.047C3.451,8.195 3.183,8.377 2.942,8.589L2.861,8.515C3.284,8.087 3.375,7.907 3.375,7.538V6C3.375,5.103 4.103,4.375 5,4.375H7.411C7.824,4.375 8.222,4.532 8.523,4.815L8.68,4.962C9.306,5.549 10.131,5.875 10.989,5.875L12.501,5.875C12.513,5.268 12.607,4.681 12.774,4.125Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M18.583,11C18.261,11 18,10.788 17.93,10.476L17.719,9.569C17.608,9.538 17.497,9.498 17.367,9.427L16.563,9.911C16.292,10.083 15.97,10.073 15.729,9.831L15.176,9.256C14.955,9.034 14.925,8.702 15.095,8.419L15.568,7.613C15.528,7.502 15.477,7.411 15.427,7.29L14.523,7.079C14.211,6.998 14,6.746 14,6.413V5.607C14,5.294 14.211,5.042 14.523,4.962L15.417,4.74C15.467,4.619 15.518,4.508 15.558,4.417L15.055,3.601C14.884,3.319 14.945,2.986 15.166,2.754L15.739,2.2C15.97,1.968 16.302,1.927 16.553,2.079L17.347,2.583C17.467,2.522 17.578,2.482 17.719,2.431L17.93,1.534C18,1.222 18.261,1 18.583,1H19.397C19.729,1 19.98,1.212 20.06,1.524L20.281,2.452C20.392,2.492 20.503,2.522 20.623,2.583L21.437,2.079C21.698,1.917 22.02,1.968 22.251,2.2L22.834,2.754C23.055,2.976 23.085,3.319 22.915,3.601L22.422,4.417C22.462,4.508 22.513,4.609 22.563,4.74L23.457,4.962C23.779,5.042 24,5.284 24,5.607V6.413C24,6.746 23.789,6.998 23.457,7.079L22.553,7.29C22.513,7.411 22.482,7.502 22.412,7.613L22.884,8.409C23.065,8.702 23.045,9.024 22.814,9.256L22.251,9.831C22.01,10.073 21.688,10.083 21.417,9.911L20.623,9.427C20.513,9.488 20.392,9.538 20.271,9.569L20.06,10.476C19.98,10.798 19.729,11 19.397,11H18.583ZM18.995,7.563C19.839,7.563 20.543,6.867 20.543,6.01C20.543,5.153 19.839,4.448 18.995,4.448C18.141,4.448 17.437,5.153 17.437,6.01C17.437,6.867 18.141,7.563 18.995,7.563Z"
android:fillColor="#000000"/>
</vector>

View file

@ -34,7 +34,7 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/chat_folder_list"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="52dp"
android:clipToPadding="false"

View file

@ -5101,14 +5101,10 @@
<string name="ChatFoldersFragment__folder_added">%1$s folder added.</string>
<!-- Option in context menu to edit the folder -->
<string name="ChatFoldersFragment__edit_folder">Edit folder</string>
<!-- Option in context menu to delete the folder -->
<string name="ChatFoldersFragment__delete_folder">Delete folder</string>
<!-- Option in context menu to add a new folder -->
<string name="ChatFoldersFragment__add_new_folder">Add new folder</string>
<!-- Option in context menu to mute all chats in the folder -->
<string name="ChatFoldersFragment__mute_all">Mute all</string>
<!-- Option in context menu to reorder the positions of the folder -->
<string name="ChatFoldersFragment__reorder_folder">Reorder folders</string>
<!-- Option in context menu to unmute all chats in the folder -->
<string name="ChatFoldersFragment__unmute_all">Unmute all</string>
<!-- Option in context menu to mark all of the chats in a folder as read -->
<string name="ChatFoldersFragment__mark_all_read">Mark all read</string>
<!-- Text describing the number of chat types in a folder -->