mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-25 19:29:54 +01:00
Release chat folders to internal users.
This commit is contained in:
committed by
Greyson Parrelli
parent
e5c122d972
commit
c4fc32988c
@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components.settings.app.chats
|
||||
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
@@ -61,6 +60,19 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
|
||||
|
||||
dividerPref()
|
||||
|
||||
if (RemoteConfig.internalUser) {
|
||||
sectionHeaderPref(R.string.ChatsSettingsFragment__chat_folders)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ChatsSettingsFragment__add_chat_folder),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_chatFoldersFragment)
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
}
|
||||
|
||||
sectionHeaderPref(R.string.ChatsSettingsFragment__keyboard)
|
||||
|
||||
switchPref(
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.folders
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
* Represents an entry in the [org.thoughtcrime.securesms.database.ChatFolderTables].
|
||||
*/
|
||||
data class ChatFolderRecord(
|
||||
val id: Long = -1,
|
||||
val name: String = "",
|
||||
val position: Int = -1,
|
||||
val includedChats: List<Long> = emptyList(),
|
||||
val excludedChats: List<Long> = emptyList(),
|
||||
val includedRecipients: Set<Recipient> = emptySet(),
|
||||
val excludedRecipients: Set<Recipient> = emptySet(),
|
||||
val showUnread: Boolean = false,
|
||||
val showMutedChats: Boolean = false,
|
||||
val showIndividualChats: Boolean = false,
|
||||
val showGroupChats: Boolean = false,
|
||||
val isMuted: Boolean = false,
|
||||
val folderType: FolderType = FolderType.CUSTOM,
|
||||
val unreadCount: Int = 0 // TODO [michelle]: unread count
|
||||
) {
|
||||
enum class FolderType(val value: Int) {
|
||||
/** Folder containing all chats */
|
||||
ALL(0),
|
||||
|
||||
/** Folder containing all 1:1 chats */
|
||||
INDIVIDUAL(1),
|
||||
|
||||
/** Folder containing group chats */
|
||||
GROUP(2),
|
||||
|
||||
/** Folder containing unread chats. */
|
||||
UNREAD(3),
|
||||
|
||||
/** Folder containing custom chosen chats */
|
||||
CUSTOM(4);
|
||||
|
||||
companion object {
|
||||
fun deserialize(value: Int): FolderType {
|
||||
return entries.firstOrNull { it.value == value } ?: CUSTOM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.folders
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Dividers
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.signal.core.ui.copied.androidx.compose.DraggableItem
|
||||
import org.signal.core.ui.copied.androidx.compose.dragContainer
|
||||
import org.signal.core.ui.copied.androidx.compose.rememberDragDropState
|
||||
import org.signal.core.util.toInt
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* Fragment that displays current and suggested chat folders
|
||||
*/
|
||||
class ChatFoldersFragment : ComposeFragment() {
|
||||
|
||||
private val viewModel: ChatFoldersViewModel by activityViewModels()
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val navController: NavController by remember { mutableStateOf(findNavController()) }
|
||||
viewModel.loadCurrentFolders(requireContext())
|
||||
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(id = R.string.ChatsSettingsFragment__chat_folders),
|
||||
onNavigationClick = { navController.popBackStack() },
|
||||
navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24),
|
||||
navigationContentDescription = stringResource(id = R.string.Material3SearchToolbar__close)
|
||||
) { contentPadding: PaddingValues ->
|
||||
FoldersScreen(
|
||||
state = state,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
onFolderClicked = {
|
||||
viewModel.setCurrentFolder(it)
|
||||
navController.safeNavigate(R.id.action_chatFoldersFragment_to_createFoldersFragment)
|
||||
},
|
||||
onAdd = { folder ->
|
||||
Toast.makeText(requireContext(), getString(R.string.ChatFoldersFragment__folder_added, folder.name), Toast.LENGTH_SHORT).show()
|
||||
viewModel.createFolder(requireContext(), folder)
|
||||
},
|
||||
onPositionUpdated = { fromIndex, toIndex -> viewModel.updatePosition(fromIndex, toIndex) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FoldersScreen(
|
||||
state: ChatFoldersSettingsState,
|
||||
modifier: Modifier = Modifier,
|
||||
onFolderClicked: (ChatFolderRecord) -> Unit = {},
|
||||
onAdd: (ChatFolderRecord) -> Unit = {},
|
||||
onPositionUpdated: (Int, Int) -> Unit = { _, _ -> }
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val dragDropState =
|
||||
rememberDragDropState(listState) { fromIndex, toIndex ->
|
||||
onPositionUpdated(fromIndex, toIndex)
|
||||
}
|
||||
|
||||
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()) }
|
||||
)
|
||||
}
|
||||
|
||||
val columnHeight = dimensionResource(id = R.dimen.chat_folder_row_height).value * state.folders.size
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.height(columnHeight.dp)
|
||||
.dragContainer(dragDropState),
|
||||
state = listState
|
||||
) {
|
||||
itemsIndexed(state.folders) { index, folder ->
|
||||
DraggableItem(dragDropState, index) { isDragging ->
|
||||
val elevation = if (isDragging) 1.dp else 0.dp
|
||||
val isAllChats = folder.folderType == ChatFolderRecord.FolderType.ALL
|
||||
FolderRow(
|
||||
icon = R.drawable.ic_chat_folder_24,
|
||||
title = if (isAllChats) stringResource(R.string.ChatFoldersFragment__all_chats) else folder.name,
|
||||
subtitle = getFolderDescription(folder),
|
||||
onClick = if (!isAllChats) {
|
||||
{ onFolderClicked(folder) }
|
||||
} else null,
|
||||
elevation = elevation,
|
||||
showDragHandle = true,
|
||||
modifier = Modifier.padding(start = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.suggestedFolders.isNotEmpty()) {
|
||||
Dividers.Default()
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.ChatFoldersFragment__suggested_folders),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 12.dp, start = 24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
state.suggestedFolders.forEach { chatFolder ->
|
||||
when (chatFolder.folderType) {
|
||||
ChatFolderRecord.FolderType.UNREAD -> {
|
||||
val title: String = stringResource(R.string.ChatFoldersFragment__unreads)
|
||||
FolderRow(
|
||||
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)
|
||||
)
|
||||
}
|
||||
ChatFolderRecord.FolderType.INDIVIDUAL -> {
|
||||
val title: String = stringResource(R.string.ChatFoldersFragment__one_on_one_chats)
|
||||
FolderRow(
|
||||
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)
|
||||
)
|
||||
}
|
||||
ChatFolderRecord.FolderType.GROUP -> {
|
||||
val title: String = stringResource(R.string.ChatFoldersFragment__groups)
|
||||
FolderRow(
|
||||
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)
|
||||
)
|
||||
}
|
||||
ChatFolderRecord.FolderType.ALL -> {
|
||||
throw IllegalStateException("All chats should not be suggested")
|
||||
}
|
||||
ChatFolderRecord.FolderType.CUSTOM -> {
|
||||
throw IllegalStateException("Custom folders should not be suggested")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getFolderDescription(folder: ChatFolderRecord): String {
|
||||
val chatTypeCount = folder.showIndividualChats.toInt() + folder.showGroupChats.toInt()
|
||||
val chatTypes = pluralStringResource(id = R.plurals.ChatFoldersFragment__d_chat_types, count = chatTypeCount, chatTypeCount)
|
||||
val includedChats = pluralStringResource(id = R.plurals.ChatFoldersFragment__d_chats, count = folder.includedChats.size, folder.includedChats.size)
|
||||
val excludedChats = pluralStringResource(id = R.plurals.ChatFoldersFragment__d_chats_excluded, count = folder.excludedChats.size, folder.excludedChats.size)
|
||||
|
||||
return remember(chatTypeCount, folder.includedChats.size, folder.excludedChats.size) {
|
||||
val description = mutableListOf<String>()
|
||||
if (chatTypeCount != 0) {
|
||||
description.add(chatTypes)
|
||||
}
|
||||
if (folder.includedChats.isNotEmpty()) {
|
||||
description.add(includedChats)
|
||||
}
|
||||
if (folder.excludedChats.isNotEmpty()) {
|
||||
description.add(excludedChats)
|
||||
}
|
||||
description.joinToString(separator = ", ")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FolderRow(
|
||||
modifier: Modifier = Modifier,
|
||||
icon: Int,
|
||||
title: String,
|
||||
subtitle: String = "",
|
||||
onClick: (() -> Unit)? = null,
|
||||
onAdd: (() -> Unit)? = null,
|
||||
elevation: Dp = 0.dp,
|
||||
showDragHandle: Boolean = false
|
||||
) {
|
||||
Row(
|
||||
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)
|
||||
} else {
|
||||
modifier
|
||||
.padding(end = 12.dp)
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = dimensionResource(id = R.dimen.chat_folder_row_height))
|
||||
.shadow(elevation = elevation)
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
|
||||
imageVector = ImageVector.vectorResource(id = icon),
|
||||
contentDescription = null,
|
||||
modifier = modifier
|
||||
.size(40.dp)
|
||||
.background(color = MaterialTheme.colorScheme.surfaceVariant, shape = CircleShape)
|
||||
.padding(8.dp)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(text = title)
|
||||
if (subtitle.isNotEmpty()) {
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (onAdd != null) {
|
||||
Buttons.Small(onClick = onAdd, modifier = modifier.padding(end = 12.dp)) {
|
||||
Text(stringResource(id = R.string.ChatFoldersFragment__add))
|
||||
}
|
||||
} else if (showDragHandle) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_drag_handle),
|
||||
contentDescription = null,
|
||||
modifier = modifier.padding(end = 12.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun ChatFolderPreview() {
|
||||
val previewFolders = listOf(
|
||||
ChatFolderRecord(
|
||||
id = 1,
|
||||
name = "Work",
|
||||
position = 1,
|
||||
showUnread = true,
|
||||
showIndividualChats = true,
|
||||
showGroupChats = true,
|
||||
showMutedChats = true,
|
||||
isMuted = false,
|
||||
folderType = ChatFolderRecord.FolderType.CUSTOM
|
||||
),
|
||||
ChatFolderRecord(
|
||||
id = 2,
|
||||
name = "Fun People",
|
||||
position = 2,
|
||||
showUnread = true,
|
||||
showIndividualChats = true,
|
||||
showGroupChats = false,
|
||||
showMutedChats = false,
|
||||
isMuted = false,
|
||||
folderType = ChatFolderRecord.FolderType.CUSTOM
|
||||
)
|
||||
)
|
||||
|
||||
Previews.Preview {
|
||||
FoldersScreen(
|
||||
ChatFoldersSettingsState(
|
||||
folders = previewFolders
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.folders
|
||||
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
|
||||
/**
|
||||
* Repository for chat folders that handles creation, deletion, listing, etc.,
|
||||
*/
|
||||
object ChatFoldersRepository {
|
||||
|
||||
fun getCurrentFolders(includeUnreadCount: Boolean = false): List<ChatFolderRecord> {
|
||||
return SignalDatabase.chatFolders.getChatFolders(includeUnreadCount)
|
||||
}
|
||||
|
||||
fun createFolder(folder: ChatFolderRecord) {
|
||||
val includedChats = folder.includedRecipients.map { recipient -> SignalDatabase.threads.getOrCreateThreadIdFor(recipient) }
|
||||
val excludedChats = folder.excludedRecipients.map { recipient -> SignalDatabase.threads.getOrCreateThreadIdFor(recipient) }
|
||||
val updatedFolder = folder.copy(
|
||||
includedChats = includedChats,
|
||||
excludedChats = excludedChats
|
||||
)
|
||||
|
||||
SignalDatabase.chatFolders.createFolder(updatedFolder)
|
||||
}
|
||||
|
||||
fun updateFolder(folder: ChatFolderRecord) {
|
||||
val includedChats = folder.includedRecipients.map { recipient -> SignalDatabase.threads.getOrCreateThreadIdFor(recipient) }
|
||||
val excludedChats = folder.excludedRecipients.map { recipient -> SignalDatabase.threads.getOrCreateThreadIdFor(recipient) }
|
||||
val updatedFolder = folder.copy(
|
||||
includedChats = includedChats,
|
||||
excludedChats = excludedChats
|
||||
)
|
||||
|
||||
SignalDatabase.chatFolders.updateFolder(updatedFolder)
|
||||
}
|
||||
|
||||
fun deleteFolder(folder: ChatFolderRecord) {
|
||||
SignalDatabase.chatFolders.deleteChatFolder(folder)
|
||||
}
|
||||
|
||||
fun updatePositions(folders: List<ChatFolderRecord>) {
|
||||
SignalDatabase.chatFolders.updatePositions(folders)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.folders
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.paged.ChatType
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* Information about chat folders. Used in [ChatFoldersViewModel].
|
||||
*/
|
||||
data class ChatFoldersSettingsState(
|
||||
val folders: List<ChatFolderRecord> = emptyList(),
|
||||
val suggestedFolders: List<ChatFolderRecord> = emptyList(),
|
||||
val originalFolder: ChatFolderRecord = ChatFolderRecord(),
|
||||
val currentFolder: ChatFolderRecord = ChatFolderRecord(),
|
||||
val showDeleteDialog: Boolean = false,
|
||||
val showConfirmationDialog: Boolean = false,
|
||||
val pendingIncludedRecipients: Set<RecipientId> = emptySet(),
|
||||
val pendingExcludedRecipients: Set<RecipientId> = emptySet(),
|
||||
val pendingChatTypes: Set<ChatType> = emptySet()
|
||||
)
|
||||
@@ -0,0 +1,313 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.folders
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.contacts.paged.ChatType
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* Maintains the state of the [ChatFoldersFragment] and [CreateFoldersFragment]
|
||||
*/
|
||||
class ChatFoldersViewModel : ViewModel() {
|
||||
|
||||
private val internalState = MutableStateFlow(ChatFoldersSettingsState())
|
||||
val state = internalState.asStateFlow()
|
||||
|
||||
fun loadCurrentFolders(context: Context) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val folders = ChatFoldersRepository.getCurrentFolders(includeUnreadCount = false)
|
||||
val suggestedFolders = getSuggestedFolders(context, folders)
|
||||
|
||||
internalState.update {
|
||||
it.copy(folders = folders, suggestedFolders = suggestedFolders)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSuggestedFolders(context: Context, currentFolders: List<ChatFolderRecord>): List<ChatFolderRecord> {
|
||||
var showIndividualSuggestion = true
|
||||
var showGroupSuggestion = true
|
||||
var showUnreadSuggestion = true
|
||||
|
||||
currentFolders
|
||||
.filter { folder -> folder.includedChats.isEmpty() && folder.excludedChats.isEmpty() }
|
||||
.forEach { folder ->
|
||||
if (folder.showIndividualChats && !folder.showGroupChats) {
|
||||
showIndividualSuggestion = false
|
||||
} else if (folder.showGroupChats && !folder.showIndividualChats) {
|
||||
showGroupSuggestion = false
|
||||
} else if (folder.showUnread && folder.showIndividualChats && folder.showGroupChats) {
|
||||
showUnreadSuggestion = false
|
||||
}
|
||||
}
|
||||
|
||||
val suggestions: MutableList<ChatFolderRecord> = mutableListOf()
|
||||
if (showIndividualSuggestion) {
|
||||
suggestions.add(
|
||||
ChatFolderRecord(
|
||||
name = context.getString(R.string.ChatFoldersFragment__one_on_one_chats),
|
||||
showIndividualChats = true,
|
||||
folderType = ChatFolderRecord.FolderType.INDIVIDUAL,
|
||||
showMutedChats = true
|
||||
)
|
||||
)
|
||||
}
|
||||
if (showGroupSuggestion) {
|
||||
suggestions.add(
|
||||
ChatFolderRecord(
|
||||
name = context.getString(R.string.ChatFoldersFragment__groups),
|
||||
showGroupChats = true,
|
||||
folderType = ChatFolderRecord.FolderType.GROUP,
|
||||
showMutedChats = true
|
||||
)
|
||||
)
|
||||
}
|
||||
if (showUnreadSuggestion) {
|
||||
suggestions.add(
|
||||
ChatFolderRecord(
|
||||
name = context.getString(R.string.ChatFoldersFragment__unreads),
|
||||
showUnread = true,
|
||||
showIndividualChats = true,
|
||||
showGroupChats = true,
|
||||
showMutedChats = true,
|
||||
folderType = ChatFolderRecord.FolderType.UNREAD
|
||||
)
|
||||
)
|
||||
}
|
||||
return suggestions
|
||||
}
|
||||
|
||||
fun setCurrentFolder(folder: ChatFolderRecord) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val includedRecipients = folder.includedChats.mapNotNull { threadId ->
|
||||
SignalDatabase.threads.getRecipientForThreadId(threadId)
|
||||
}
|
||||
val excludedRecipients = folder.excludedChats.mapNotNull { threadId ->
|
||||
SignalDatabase.threads.getRecipientForThreadId(threadId)
|
||||
}
|
||||
|
||||
val updatedFolder = folder.copy(
|
||||
includedRecipients = includedRecipients.toSet(),
|
||||
excludedRecipients = excludedRecipients.toSet()
|
||||
)
|
||||
|
||||
internalState.update {
|
||||
it.copy(originalFolder = updatedFolder, currentFolder = updatedFolder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateName(name: String) {
|
||||
val updatedFolder = internalState.value.currentFolder.copy(
|
||||
name = name.substring(0, minOf(name.length, 32))
|
||||
)
|
||||
|
||||
internalState.update {
|
||||
it.copy(currentFolder = updatedFolder)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleShowUnread(showUnread: Boolean) {
|
||||
val updatedFolder = internalState.value.currentFolder.copy(
|
||||
showUnread = showUnread
|
||||
)
|
||||
|
||||
internalState.update {
|
||||
it.copy(currentFolder = updatedFolder)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleShowMutedChats(showMuted: Boolean) {
|
||||
val updatedFolder = internalState.value.currentFolder.copy(
|
||||
showMutedChats = showMuted
|
||||
)
|
||||
|
||||
internalState.update {
|
||||
it.copy(currentFolder = updatedFolder)
|
||||
}
|
||||
}
|
||||
|
||||
fun showDeleteDialog(show: Boolean) {
|
||||
internalState.update {
|
||||
it.copy(showDeleteDialog = show)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteFolder() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
ChatFoldersRepository.deleteFolder(internalState.value.originalFolder)
|
||||
|
||||
internalState.update {
|
||||
it.copy(showDeleteDialog = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showConfirmationDialog(show: Boolean) {
|
||||
internalState.update {
|
||||
it.copy(showConfirmationDialog = show)
|
||||
}
|
||||
}
|
||||
|
||||
fun createFolder(context: Context, folder: ChatFolderRecord? = null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val currentFolder = folder ?: internalState.value.currentFolder
|
||||
ChatFoldersRepository.createFolder(currentFolder)
|
||||
loadCurrentFolders(context)
|
||||
|
||||
internalState.update {
|
||||
it.copy(showConfirmationDialog = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePosition(fromIndex: Int, toIndex: Int) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val folders = state.value.folders.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
|
||||
val updatedFolders = folders.mapIndexed { index, chatFolderRecord ->
|
||||
chatFolderRecord.copy(position = index)
|
||||
}
|
||||
ChatFoldersRepository.updatePositions(updatedFolders)
|
||||
|
||||
internalState.update {
|
||||
it.copy(folders = updatedFolders)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateFolder(context: Context) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
ChatFoldersRepository.updateFolder(internalState.value.currentFolder)
|
||||
loadCurrentFolders(context)
|
||||
|
||||
internalState.update {
|
||||
it.copy(showConfirmationDialog = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setPendingChats() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val currentFolder = internalState.value.currentFolder
|
||||
val includedChats = currentFolder.includedRecipients.map { recipient -> recipient.id }.toMutableSet()
|
||||
val excludedChats = currentFolder.excludedRecipients.map { recipient -> recipient.id }.toMutableSet()
|
||||
|
||||
val chatTypes: MutableSet<ChatType> = mutableSetOf()
|
||||
if (currentFolder.showIndividualChats) {
|
||||
chatTypes.add(ChatType.INDIVIDUAL)
|
||||
}
|
||||
if (currentFolder.showGroupChats) {
|
||||
chatTypes.add(ChatType.GROUPS)
|
||||
}
|
||||
|
||||
internalState.update {
|
||||
it.copy(
|
||||
pendingIncludedRecipients = includedChats,
|
||||
pendingExcludedRecipients = excludedChats,
|
||||
pendingChatTypes = chatTypes
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addIncludedChat(recipientId: RecipientId) {
|
||||
val includedChats = internalState.value.pendingIncludedRecipients.plus(recipientId)
|
||||
internalState.update {
|
||||
it.copy(pendingIncludedRecipients = includedChats)
|
||||
}
|
||||
}
|
||||
|
||||
fun addExcludedChat(recipientId: RecipientId) {
|
||||
val excludedChats = internalState.value.pendingExcludedRecipients.plus(recipientId)
|
||||
internalState.update {
|
||||
it.copy(pendingExcludedRecipients = excludedChats)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeIncludedChat(recipientId: RecipientId) {
|
||||
val includedChats = internalState.value.pendingIncludedRecipients.minus(recipientId)
|
||||
internalState.update {
|
||||
it.copy(pendingIncludedRecipients = includedChats)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeExcludedChat(recipientId: RecipientId) {
|
||||
val excludedChats = internalState.value.pendingExcludedRecipients.minus(recipientId)
|
||||
internalState.update {
|
||||
it.copy(pendingExcludedRecipients = excludedChats)
|
||||
}
|
||||
}
|
||||
|
||||
fun addChatType(chatType: ChatType) {
|
||||
val updatedChatTypes = internalState.value.pendingChatTypes.plus(chatType)
|
||||
internalState.update {
|
||||
it.copy(
|
||||
pendingChatTypes = updatedChatTypes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeChatType(chatType: ChatType) {
|
||||
val updatedChatTypes = internalState.value.pendingChatTypes.minus(chatType)
|
||||
internalState.update {
|
||||
it.copy(
|
||||
pendingChatTypes = updatedChatTypes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun savePendingChats() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val updatedFolder = internalState.value.currentFolder
|
||||
val includedChatIds = internalState.value.pendingIncludedRecipients
|
||||
val excludedChatIds = internalState.value.pendingExcludedRecipients
|
||||
val showIndividualChats = internalState.value.pendingChatTypes.contains(ChatType.INDIVIDUAL)
|
||||
val showGroupChats = internalState.value.pendingChatTypes.contains(ChatType.GROUPS)
|
||||
|
||||
val includedRecipients = includedChatIds.map(Recipient::resolved).toSet()
|
||||
val excludedRecipients = excludedChatIds.map(Recipient::resolved).toSet()
|
||||
|
||||
internalState.update {
|
||||
it.copy(
|
||||
currentFolder = updatedFolder.copy(
|
||||
includedRecipients = includedRecipients,
|
||||
excludedRecipients = excludedRecipients,
|
||||
showIndividualChats = showIndividualChats,
|
||||
showGroupChats = showGroupChats
|
||||
),
|
||||
pendingIncludedRecipients = emptySet(),
|
||||
pendingExcludedRecipients = emptySet()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun enableButton(): Boolean {
|
||||
return internalState.value.pendingIncludedRecipients.isNotEmpty() ||
|
||||
internalState.value.pendingChatTypes.isNotEmpty() ||
|
||||
internalState.value.pendingExcludedRecipients.isNotEmpty()
|
||||
}
|
||||
|
||||
fun hasChanges(): Boolean {
|
||||
val currentFolder = state.value.currentFolder
|
||||
val originalFolder = state.value.originalFolder
|
||||
|
||||
return if (currentFolder.id == -1L) {
|
||||
currentFolder.name.isNotEmpty() &&
|
||||
(currentFolder.includedRecipients.isNotEmpty() || currentFolder.showIndividualChats || currentFolder.showGroupChats)
|
||||
} else {
|
||||
originalFolder != currentFolder ||
|
||||
originalFolder.includedRecipients != currentFolder.includedRecipients ||
|
||||
originalFolder.excludedRecipients != currentFolder.excludedRecipients
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.folders
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact
|
||||
import org.thoughtcrime.securesms.contacts.paged.ChatType
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import java.util.Optional
|
||||
import java.util.function.Consumer
|
||||
|
||||
class ChooseChatsFragment : LoggingFragment(), ContactSelectionListFragment.OnContactSelectedListener {
|
||||
|
||||
private val viewModel: ChatFoldersViewModel by activityViewModels()
|
||||
|
||||
private var includeChatsMode: Boolean = true
|
||||
|
||||
private lateinit var contactFilterView: ContactFilterView
|
||||
private lateinit var doneButton: MaterialButton
|
||||
private lateinit var selectionFragment: ContactSelectionListFragment
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
includeChatsMode = arguments?.getBoolean(KEY_INCLUDE_CHATS) ?: true
|
||||
val currentSelection: Set<RecipientId> = if (includeChatsMode) {
|
||||
viewModel.state.value.pendingExcludedRecipients
|
||||
} else {
|
||||
viewModel.state.value.pendingIncludedRecipients
|
||||
}
|
||||
|
||||
childFragmentManager.addFragmentOnAttachListener { _, fragment ->
|
||||
fragment.arguments = Bundle().apply {
|
||||
putInt(ContactSelectionListFragment.DISPLAY_MODE, getDefaultDisplayMode())
|
||||
putBoolean(ContactSelectionListFragment.REFRESHABLE, false)
|
||||
putBoolean(ContactSelectionListFragment.RECENTS, true)
|
||||
putParcelable(ContactSelectionListFragment.SELECTION_LIMITS, SelectionLimits.NO_LIMITS)
|
||||
putParcelableArrayList(ContactSelectionListFragment.CURRENT_SELECTION, ArrayList<RecipientId>(currentSelection))
|
||||
putBoolean(ContactSelectionListFragment.INCLUDE_CHAT_TYPES, includeChatsMode)
|
||||
putBoolean(ContactSelectionListFragment.HIDE_COUNT, true)
|
||||
putBoolean(ContactSelectionListFragment.DISPLAY_CHIPS, true)
|
||||
putBoolean(ContactSelectionListFragment.CAN_SELECT_SELF, true)
|
||||
putBoolean(ContactSelectionListFragment.RV_CLIP, false)
|
||||
putInt(ContactSelectionListFragment.RV_PADDING_BOTTOM, ViewUtil.dpToPx(60))
|
||||
}
|
||||
}
|
||||
|
||||
return inflater.inflate(R.layout.choose_chats_fragment, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
|
||||
if (includeChatsMode) {
|
||||
toolbar.setTitle(R.string.CreateFoldersFragment__included_chats)
|
||||
} else {
|
||||
toolbar.setTitle(R.string.CreateFoldersFragment__exceptions)
|
||||
}
|
||||
toolbar.setNavigationOnClickListener { findNavController().popBackStack() }
|
||||
|
||||
selectionFragment = childFragmentManager.findFragmentById(R.id.contact_selection_list) as ContactSelectionListFragment
|
||||
contactFilterView = view.findViewById(R.id.contact_filter_edit_text)
|
||||
contactFilterView.setOnFilterChangedListener {
|
||||
if (it.isNullOrEmpty()) {
|
||||
selectionFragment.resetQueryFilter()
|
||||
} else {
|
||||
selectionFragment.setQueryFilter(it)
|
||||
}
|
||||
}
|
||||
|
||||
doneButton = view.findViewById(R.id.done_button)
|
||||
doneButton.setOnClickListener {
|
||||
viewModel.savePendingChats()
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
updateEnabledButton()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
if (includeChatsMode && viewModel.state.value.pendingChatTypes.contains(ChatType.INDIVIDUAL)) {
|
||||
selectionFragment.markContactSelected(SelectedContact.forChatType(ChatType.INDIVIDUAL))
|
||||
}
|
||||
if (includeChatsMode && viewModel.state.value.pendingChatTypes.contains(ChatType.GROUPS)) {
|
||||
selectionFragment.markContactSelected(SelectedContact.forChatType(ChatType.GROUPS))
|
||||
}
|
||||
|
||||
val activeSelection: Set<RecipientId> = if (includeChatsMode) {
|
||||
viewModel.state.value.pendingIncludedRecipients
|
||||
} else {
|
||||
viewModel.state.value.pendingExcludedRecipients
|
||||
}
|
||||
|
||||
selectionFragment.markSelected(activeSelection)
|
||||
}
|
||||
|
||||
override fun onBeforeContactSelected(isFromUnknownSearchKey: Boolean, recipientId: Optional<RecipientId>, number: String?, chatType: Optional<ChatType>, callback: Consumer<Boolean>) {
|
||||
if (recipientId.isPresent) {
|
||||
if (includeChatsMode) {
|
||||
viewModel.addIncludedChat(recipientId.get())
|
||||
} else {
|
||||
viewModel.addExcludedChat(recipientId.get())
|
||||
}
|
||||
callback.accept(true)
|
||||
} else if (chatType.isPresent) {
|
||||
viewModel.addChatType(chatType.get())
|
||||
callback.accept(true)
|
||||
} else {
|
||||
callback.accept(false)
|
||||
}
|
||||
updateEnabledButton()
|
||||
}
|
||||
|
||||
override fun onContactDeselected(recipientId: Optional<RecipientId>, number: String?, chatType: Optional<ChatType>) {
|
||||
if (recipientId.isPresent) {
|
||||
if (includeChatsMode) {
|
||||
viewModel.removeIncludedChat(recipientId.get())
|
||||
} else {
|
||||
viewModel.removeExcludedChat(recipientId.get())
|
||||
}
|
||||
} else if (chatType.isPresent) {
|
||||
viewModel.removeChatType(chatType.get())
|
||||
}
|
||||
updateEnabledButton()
|
||||
}
|
||||
|
||||
override fun onSelectionChanged() = Unit
|
||||
|
||||
private fun getDefaultDisplayMode(): Int {
|
||||
return ContactSelectionDisplayMode.FLAG_PUSH or
|
||||
ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS or
|
||||
ContactSelectionDisplayMode.FLAG_HIDE_NEW or
|
||||
ContactSelectionDisplayMode.FLAG_GROUPS_AFTER_CONTACTS or
|
||||
ContactSelectionDisplayMode.FLAG_HIDE_GROUPS_V1 or
|
||||
ContactSelectionDisplayMode.FLAG_SELF
|
||||
}
|
||||
|
||||
private fun updateEnabledButton() {
|
||||
doneButton.isEnabled = viewModel.enableButton()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ChooseChatsFragment::class.java)
|
||||
private val KEY_INCLUDE_CHATS = "include_chats"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.folders
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Dialogs
|
||||
import org.signal.core.ui.Dividers
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.AvatarImage
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* Fragment that allows user to create, edit, or delete an individual folder
|
||||
*/
|
||||
class CreateFoldersFragment : ComposeFragment() {
|
||||
|
||||
private val viewModel: ChatFoldersViewModel by activityViewModels()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (viewModel.hasChanges()) {
|
||||
viewModel.showConfirmationDialog(true)
|
||||
} else {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val navController: NavController by remember { mutableStateOf(findNavController()) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val isNewFolder = state.originalFolder.id == -1L
|
||||
|
||||
Scaffolds.Settings(
|
||||
title = if (isNewFolder) stringResource(id = R.string.CreateFoldersFragment__create_a_folder) else stringResource(id = R.string.CreateFoldersFragment__edit_folder),
|
||||
onNavigationClick = {
|
||||
if (viewModel.hasChanges()) {
|
||||
viewModel.showConfirmationDialog(true)
|
||||
} else {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24),
|
||||
navigationContentDescription = stringResource(id = R.string.Material3SearchToolbar__close)
|
||||
) { contentPadding: PaddingValues ->
|
||||
CreateFolderScreen(
|
||||
state = state,
|
||||
focusRequester = focusRequester,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
isNewFolder = isNewFolder,
|
||||
hasChanges = viewModel.hasChanges(),
|
||||
onAddChat = {
|
||||
viewModel.setPendingChats()
|
||||
navController.safeNavigate(CreateFoldersFragmentDirections.actionCreateFoldersFragmentToChooseChatsFragment(true))
|
||||
},
|
||||
onRemoveChat = {
|
||||
viewModel.setPendingChats()
|
||||
navController.safeNavigate(CreateFoldersFragmentDirections.actionCreateFoldersFragmentToChooseChatsFragment(false))
|
||||
},
|
||||
onNameChange = { viewModel.updateName(it) },
|
||||
onToggleShowUnread = { viewModel.toggleShowUnread(it) },
|
||||
onToggleShowMuted = { viewModel.toggleShowMutedChats(it) },
|
||||
onDeleteClicked = { viewModel.showDeleteDialog(true) },
|
||||
onDeleteConfirmed = {
|
||||
viewModel.deleteFolder()
|
||||
navController.popBackStack()
|
||||
},
|
||||
onDeleteDismissed = {
|
||||
viewModel.showDeleteDialog(false)
|
||||
},
|
||||
onCreateConfirmed = { shouldExit ->
|
||||
if (isNewFolder) {
|
||||
viewModel.createFolder(requireContext())
|
||||
} else {
|
||||
viewModel.updateFolder(requireContext())
|
||||
}
|
||||
if (shouldExit) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
onCreateDismissed = { shouldExit ->
|
||||
viewModel.showConfirmationDialog(false)
|
||||
if (shouldExit) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CreateFolderScreen(
|
||||
state: ChatFoldersSettingsState,
|
||||
focusRequester: FocusRequester,
|
||||
modifier: Modifier = Modifier,
|
||||
isNewFolder: Boolean = true,
|
||||
hasChanges: Boolean = false,
|
||||
onAddChat: () -> Unit = {},
|
||||
onRemoveChat: () -> Unit = {},
|
||||
onNameChange: (String) -> Unit = {},
|
||||
onToggleShowUnread: (Boolean) -> Unit = {},
|
||||
onToggleShowMuted: (Boolean) -> Unit = {},
|
||||
onDeleteClicked: () -> Unit = {},
|
||||
onDeleteConfirmed: () -> Unit = {},
|
||||
onDeleteDismissed: () -> Unit = {},
|
||||
onCreateConfirmed: (Boolean) -> Unit = {},
|
||||
onCreateDismissed: (Boolean) -> Unit = {}
|
||||
) {
|
||||
if (state.showDeleteDialog) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = "",
|
||||
body = stringResource(id = R.string.CreateFoldersFragment__delete_this_chat_folder),
|
||||
confirm = stringResource(id = R.string.delete),
|
||||
onConfirm = onDeleteConfirmed,
|
||||
dismiss = stringResource(id = android.R.string.cancel),
|
||||
onDismiss = onDeleteDismissed
|
||||
)
|
||||
} else if (state.showConfirmationDialog && isNewFolder) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(id = R.string.CreateFoldersFragment__create_folder_title),
|
||||
body = stringResource(id = R.string.CreateFoldersFragment__do_you_want_to_create, state.currentFolder.name),
|
||||
confirm = stringResource(id = R.string.CreateFoldersFragment__create_folder),
|
||||
onConfirm = { onCreateConfirmed(false) },
|
||||
dismiss = stringResource(id = R.string.CreateFoldersFragment__discard),
|
||||
onDismiss = { onCreateDismissed(true) },
|
||||
onDismissRequest = { onCreateDismissed(false) }
|
||||
)
|
||||
} else if (state.showConfirmationDialog) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(id = R.string.CreateFoldersFragment__save_changes_title),
|
||||
body = stringResource(id = R.string.CreateFoldersFragment__do_you_want_to_save),
|
||||
confirm = stringResource(id = R.string.CreateFoldersFragment__save_changes),
|
||||
onConfirm = { onCreateConfirmed(false) },
|
||||
dismiss = stringResource(id = R.string.CreateFoldersFragment__discard),
|
||||
onDismiss = { onCreateDismissed(true) },
|
||||
onDismissRequest = { onCreateDismissed(false) }
|
||||
)
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn {
|
||||
item {
|
||||
TextField(
|
||||
value = state.currentFolder.name,
|
||||
label = { Text(text = stringResource(id = R.string.CreateFoldersFragment__folder_name)) },
|
||||
onValueChange = onNameChange,
|
||||
singleLine = true,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
.padding(top = 16.dp, bottom = 12.dp, start = 20.dp, end = 28.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.CreateFoldersFragment__included_chats),
|
||||
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.CreateFoldersFragment__add_chats),
|
||||
onClick = onAddChat,
|
||||
modifier = Modifier.padding(start = 12.dp)
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
||||
if (state.currentFolder.showGroupChats) {
|
||||
FolderRow(
|
||||
icon = R.drawable.symbol_group_light_20,
|
||||
title = stringResource(R.string.ChatFoldersFragment__groups),
|
||||
onClick = onAddChat,
|
||||
modifier = Modifier.padding(start = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(state.currentFolder.includedRecipients.toList()) { recipient ->
|
||||
ChatRow(
|
||||
recipient = recipient,
|
||||
onClick = onAddChat
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.CreateFoldersFragment__choose_chats_you_want),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 12.dp, bottom = 12.dp, start = 24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.CreateFoldersFragment__exceptions),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(top = 24.dp, bottom = 12.dp, end = 12.dp, start = 24.dp)
|
||||
)
|
||||
FolderRow(
|
||||
icon = R.drawable.symbol_plus_compact_16,
|
||||
title = stringResource(R.string.CreateFoldersFragment__exclude_chats),
|
||||
onClick = onRemoveChat,
|
||||
modifier = Modifier.padding(start = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
items(state.currentFolder.excludedRecipients.toList()) { recipient ->
|
||||
ChatRow(
|
||||
recipient = recipient,
|
||||
onClick = onRemoveChat
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.CreateFoldersFragment__choose_chats_you_do_not_want),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 12.dp, bottom = 12.dp, start = 24.dp, end = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Dividers.Default()
|
||||
ShowUnreadSection(state, onToggleShowUnread)
|
||||
ShowMutedSection(state, onToggleShowMuted)
|
||||
|
||||
if (!isNewFolder) {
|
||||
Dividers.Default()
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.CreateFoldersFragment__delete_folder),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier
|
||||
.clickable { onDeleteClicked() }
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp, top = 16.dp, bottom = 32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
item { Spacer(modifier = Modifier.height(60.dp)) }
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges && isNewFolder) {
|
||||
Buttons.MediumTonal(
|
||||
onClick = { onCreateConfirmed(true) },
|
||||
modifier = modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 16.dp, bottom = 16.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.CreateFoldersFragment__create))
|
||||
}
|
||||
} else if (!isNewFolder) {
|
||||
Buttons.MediumTonal(
|
||||
enabled = hasChanges,
|
||||
onClick = { onCreateConfirmed(true) },
|
||||
modifier = modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 16.dp, bottom = 16.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.CreateFoldersFragment__save))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowUnreadSection(state: ChatFoldersSettingsState, onToggleShowUnread: (Boolean) -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 24.dp)
|
||||
.defaultMinSize(minHeight = 92.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.CreateFoldersFragment__only_show_unread_chats),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.CreateFoldersFragment__when_enabled_only_chats),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = state.currentFolder.showUnread,
|
||||
onCheckedChange = onToggleShowUnread
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowMutedSection(state: ChatFoldersSettingsState, onToggleShowMuted: (Boolean) -> Unit) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 24.dp)
|
||||
.defaultMinSize(minHeight = 56.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.CreateFoldersFragment__include_muted_chats),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = state.currentFolder.showMutedChats,
|
||||
onCheckedChange = onToggleShowMuted
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun CreateFolderPreview() {
|
||||
val previewFolder = ChatFolderRecord(id = 1, name = "WIP")
|
||||
|
||||
Previews.Preview {
|
||||
CreateFolderScreen(
|
||||
state = ChatFoldersSettingsState(currentFolder = previewFolder),
|
||||
focusRequester = FocusRequester(),
|
||||
isNewFolder = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun EditFolderPreview() {
|
||||
val previewFolder = ChatFolderRecord(id = 1, name = "Work")
|
||||
|
||||
Previews.Preview {
|
||||
CreateFolderScreen(
|
||||
state = ChatFoldersSettingsState(originalFolder = previewFolder),
|
||||
focusRequester = FocusRequester(),
|
||||
isNewFolder = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatRow(
|
||||
recipient: Recipient,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (() -> Unit)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.clickable(onClick = onClick)
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 64.dp)
|
||||
) {
|
||||
if (LocalInspectionMode.current) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Person,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(start = 24.dp, end = 16.dp)
|
||||
.size(40.dp)
|
||||
.background(
|
||||
color = Color.Red,
|
||||
shape = CircleShape
|
||||
)
|
||||
)
|
||||
} else {
|
||||
AvatarImage(
|
||||
recipient = recipient,
|
||||
modifier = Modifier
|
||||
.padding(start = 24.dp, end = 16.dp)
|
||||
.size(40.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Text(text = recipient.getShortDisplayName(LocalContext.current))
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode
|
||||
import org.thoughtcrime.securesms.contacts.paged.ChatType
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
@@ -106,7 +107,7 @@ class SelectRecipientsFragment : LoggingFragment(), ContactSelectionListFragment
|
||||
ContactSelectionDisplayMode.FLAG_HIDE_GROUPS_V1
|
||||
}
|
||||
|
||||
override fun onBeforeContactSelected(isFromUnknownSearchKey: Boolean, recipientId: Optional<RecipientId>, number: String?, callback: Consumer<Boolean>) {
|
||||
override fun onBeforeContactSelected(isFromUnknownSearchKey: Boolean, recipientId: Optional<RecipientId>, number: String?, chatType: Optional<ChatType>, callback: Consumer<Boolean>) {
|
||||
if (recipientId.isPresent) {
|
||||
viewModel.select(recipientId.get())
|
||||
callback.accept(true)
|
||||
@@ -116,7 +117,7 @@ class SelectRecipientsFragment : LoggingFragment(), ContactSelectionListFragment
|
||||
}
|
||||
}
|
||||
|
||||
override fun onContactDeselected(recipientId: Optional<RecipientId>, number: String?) {
|
||||
override fun onContactDeselected(recipientId: Optional<RecipientId>, number: String?, chatType: Optional<ChatType>) {
|
||||
if (recipientId.isPresent) {
|
||||
viewModel.deselect(recipientId.get())
|
||||
updateAddToProfile()
|
||||
|
||||
Reference in New Issue
Block a user