mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-03-02 15:36:32 +00:00
Allow adding and removing from context menu.
This commit is contained in:
committed by
Greyson Parrelli
parent
94d6bfd9ad
commit
6fcfd8fdb1
@@ -1,17 +1,23 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.folders
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
* Represents an entry in the [org.thoughtcrime.securesms.database.ChatFolderTables].
|
||||
*/
|
||||
@Parcelize
|
||||
data class ChatFolderRecord(
|
||||
val id: Long = -1,
|
||||
val name: String = "",
|
||||
val position: Int = -1,
|
||||
val includedChats: List<Long> = emptyList(),
|
||||
val excludedChats: List<Long> = emptyList(),
|
||||
@IgnoredOnParcel
|
||||
val includedRecipients: Set<Recipient> = emptySet(),
|
||||
@IgnoredOnParcel
|
||||
val excludedRecipients: Set<Recipient> = emptySet(),
|
||||
val showUnread: Boolean = false,
|
||||
val showMutedChats: Boolean = true,
|
||||
@@ -20,7 +26,7 @@ data class ChatFolderRecord(
|
||||
val isMuted: Boolean = false,
|
||||
val folderType: FolderType = FolderType.CUSTOM,
|
||||
val unreadCount: Int = 0
|
||||
) {
|
||||
) : Parcelable {
|
||||
enum class FolderType(val value: Int) {
|
||||
/** Folder containing all chats */
|
||||
ALL(0),
|
||||
|
||||
@@ -135,7 +135,7 @@ fun FoldersScreen(
|
||||
val elevation = if (isDragging) 1.dp else 0.dp
|
||||
val isAllChats = folder.folderType == ChatFolderRecord.FolderType.ALL
|
||||
FolderRow(
|
||||
icon = R.drawable.ic_chat_folder_24,
|
||||
icon = R.drawable.symbol_folder_24,
|
||||
title = if (isAllChats) stringResource(R.string.ChatFoldersFragment__all_chats) else folder.name,
|
||||
subtitle = getFolderDescription(folder),
|
||||
onClick = if (!isAllChats) {
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
package org.thoughtcrime.securesms.conversationlist
|
||||
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.bundleOf
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.signal.core.util.getParcelableArrayListCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Bottom sheet shown when choosing to add a chat to a folder
|
||||
*/
|
||||
class AddToFolderBottomSheet private constructor() : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
override val peekHeightPercentage: Float = 1f
|
||||
|
||||
private val viewModel by viewModel { ConversationListViewModel(isArchived = false) }
|
||||
|
||||
companion object {
|
||||
private const val ARG_FOLDERS = "argument.folders"
|
||||
private const val ARG_THREAD_ID = "argument.thread.id"
|
||||
|
||||
@JvmStatic
|
||||
fun showChatFolderSheet(folders: List<ChatFolderRecord>, threadId: Long): ComposeBottomSheetDialogFragment {
|
||||
return AddToFolderBottomSheet().apply {
|
||||
arguments = bundleOf(
|
||||
ARG_FOLDERS to folders,
|
||||
ARG_THREAD_ID to threadId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
val folders = arguments?.getParcelableArrayListCompat(ARG_FOLDERS, ChatFolderRecord::class.java)?.filter { it.folderType != ChatFolderRecord.FolderType.ALL }
|
||||
val threadId = arguments?.getLong(ARG_THREAD_ID)
|
||||
|
||||
AddToChatFolderSheetContent(
|
||||
folders = remember { folders ?: emptyList() },
|
||||
onClick = { folder ->
|
||||
if (threadId != null) {
|
||||
viewModel.addToFolder(folder.id, threadId)
|
||||
Toast.makeText(context, requireContext().getString(R.string.AddToFolderBottomSheet_added_to_s, folder.name), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
dismissAllowingStateLoss()
|
||||
},
|
||||
onCreate = {
|
||||
requireContext().startActivity(AppSettingsActivity.createChatFolder(requireContext(), -1))
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddToChatFolderSheetContent(
|
||||
folders: List<ChatFolderRecord>,
|
||||
onClick: (ChatFolderRecord) -> Unit = {},
|
||||
onCreate: () -> Unit = {}
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.AddToFolderBottomSheet_choose_a_folder),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(top = 30.dp)
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp, end = 24.dp, top = 36.dp, bottom = 60.dp)
|
||||
.background(color = MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(18.dp))
|
||||
) {
|
||||
items(folders) { folder ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onClick(folder) })
|
||||
.padding(start = 24.dp)
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = dimensionResource(id = R.dimen.chat_folder_row_height))
|
||||
) {
|
||||
Image(
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_folder_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(color = MaterialTheme.colorScheme.surfaceVariant, shape = CircleShape)
|
||||
.padding(8.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = folder.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onCreate)
|
||||
.padding(start = 24.dp)
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = dimensionResource(id = R.dimen.chat_folder_row_height))
|
||||
) {
|
||||
Image(
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_plus_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(color = MaterialTheme.colorScheme.surfaceVariant, shape = CircleShape)
|
||||
.padding(8.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.ChatFoldersFragment__create_a_folder),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun AddToChatFolderSheetContentPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
AddToChatFolderSheetContent(
|
||||
folders = listOf(ChatFolderRecord(name = "Friends"), ChatFolderRecord(name = "Work"))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ class ChatFolderAdapter(val callbacks: Callbacks) : MappingAdapter() {
|
||||
val folder = model.chatFolder
|
||||
name.text = getName(itemView.context, folder)
|
||||
unreadCount.visible = folder.unreadCount > 0
|
||||
unreadCount.text = folder.unreadCount.toString()
|
||||
unreadCount.text = if (folder.unreadCount > 99) itemView.context.getString(R.string.ChatFolderAdapter__99p) else folder.unreadCount.toString()
|
||||
itemView.setOnClickListener {
|
||||
callbacks.onChatFolderClicked(model.chatFolder)
|
||||
}
|
||||
|
||||
@@ -166,6 +166,7 @@ import org.thoughtcrime.securesms.stories.tabs.ConversationListTab;
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel;
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil;
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig;
|
||||
@@ -1466,6 +1467,14 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
if (conversation.getThreadRecord().isArchived()) {
|
||||
items.add(new ActionItem(R.drawable.symbol_archive_up_24, getResources().getString(R.string.ConversationListFragment_unarchive), () -> handleArchive(id, false)));
|
||||
} else {
|
||||
if (RemoteConfig.internalUser() && viewModel.getCurrentFolder().getFolderType() == ChatFolderRecord.FolderType.ALL) {
|
||||
List<ChatFolderRecord> folders = viewModel.getFolders().stream().map(ChatFolderMappingModel::getChatFolder).collect(Collectors.toList());
|
||||
items.add(new ActionItem(R.drawable.symbol_folder_add, getString(R.string.ConversationListFragment_add_to_folder), () ->
|
||||
AddToFolderBottomSheet.showChatFolderSheet(folders, conversation.getThreadRecord().getThreadId()).show(getParentFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
));
|
||||
} else if (RemoteConfig.internalUser()){
|
||||
items.add(new ActionItem(R.drawable.symbol_folder_minus, getString(R.string.ConversationListFragment_remove_from_folder), () -> viewModel.removeChatFromFolder(conversation.getThreadRecord().getThreadId())));
|
||||
}
|
||||
items.add(new ActionItem(R.drawable.symbol_archive_24, getResources().getString(R.string.ConversationListFragment_archive), () -> handleArchive(id, false)));
|
||||
}
|
||||
|
||||
|
||||
@@ -297,6 +297,18 @@ class ConversationListViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun removeChatFromFolder(threadId: Long) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
SignalDatabase.chatFolders.removeFromFolder(currentFolder.id, threadId)
|
||||
}
|
||||
}
|
||||
|
||||
fun addToFolder(folderId: Long, threadId: Long) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
SignalDatabase.chatFolders.addToFolder(folderId, threadId)
|
||||
}
|
||||
}
|
||||
|
||||
private data class ConversationListState(
|
||||
val chatFolders: List<ChatFolderMappingModel> = emptyList(),
|
||||
val currentFolder: ChatFolderRecord = ChatFolderRecord(),
|
||||
|
||||
@@ -99,12 +99,12 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
|
||||
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
$CHAT_FOLDER_ID INTEGER NOT NULL REFERENCES ${ChatFolderTable.TABLE_NAME} (${ChatFolderTable.ID}) ON DELETE CASCADE,
|
||||
$THREAD_ID INTEGER NOT NULL REFERENCES ${ThreadTable.TABLE_NAME} (${ThreadTable.ID}) ON DELETE CASCADE,
|
||||
$MEMBERSHIP_TYPE INTEGER DEFAULT 1
|
||||
$MEMBERSHIP_TYPE INTEGER DEFAULT 1,
|
||||
UNIQUE(${CHAT_FOLDER_ID}, ${THREAD_ID}) ON CONFLICT REPLACE
|
||||
)
|
||||
"""
|
||||
|
||||
val CREATE_INDEXES = arrayOf(
|
||||
"CREATE INDEX chat_folder_membership_chat_folder_id_index ON $TABLE_NAME ($CHAT_FOLDER_ID)",
|
||||
"CREATE INDEX chat_folder_membership_thread_id_index ON $TABLE_NAME ($THREAD_ID)",
|
||||
"CREATE INDEX chat_folder_membership_membership_type_index ON $TABLE_NAME ($MEMBERSHIP_TYPE)"
|
||||
)
|
||||
@@ -347,6 +347,40 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a thread from a chat folder
|
||||
*/
|
||||
fun removeFromFolder(folderId: Long, threadId: Long) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
db.insertInto(ChatFolderMembershipTable.TABLE_NAME)
|
||||
.values(
|
||||
ChatFolderMembershipTable.CHAT_FOLDER_ID to folderId,
|
||||
ChatFolderMembershipTable.THREAD_ID to threadId,
|
||||
ChatFolderMembershipTable.MEMBERSHIP_TYPE to MembershipType.EXCLUDED.value
|
||||
)
|
||||
.run(SQLiteDatabase.CONFLICT_REPLACE)
|
||||
|
||||
AppDependencies.databaseObserver.notifyChatFolderObservers()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a thread to a chat folder
|
||||
*/
|
||||
fun addToFolder(folderId: Long, threadId: Long) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
db.insertInto(ChatFolderMembershipTable.TABLE_NAME)
|
||||
.values(
|
||||
ChatFolderMembershipTable.CHAT_FOLDER_ID to folderId,
|
||||
ChatFolderMembershipTable.THREAD_ID to threadId,
|
||||
ChatFolderMembershipTable.MEMBERSHIP_TYPE to MembershipType.INCLUDED.value
|
||||
)
|
||||
.run(SQLiteDatabase.CONFLICT_REPLACE)
|
||||
|
||||
AppDependencies.databaseObserver.notifyChatFolderObservers()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Collection<Long>.toContentValues(chatFolderId: Long, membershipType: MembershipType): List<ContentValues> {
|
||||
return map {
|
||||
contentValuesOf(
|
||||
|
||||
@@ -110,6 +110,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V250_ClearUploadTim
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V251_ArchiveTransferStateIndex
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V252_AttachmentOffloadRestoredAtColumn
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V253_CreateChatFolderTables
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V254_AddChatFolderConstraint
|
||||
|
||||
/**
|
||||
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
|
||||
@@ -222,10 +223,11 @@ object SignalDatabaseMigrations {
|
||||
250 to V250_ClearUploadTimestampV2,
|
||||
251 to V251_ArchiveTransferStateIndex,
|
||||
252 to V252_AttachmentOffloadRestoredAtColumn,
|
||||
253 to V253_CreateChatFolderTables
|
||||
253 to V253_CreateChatFolderTables,
|
||||
254 to V254_AddChatFolderConstraint
|
||||
)
|
||||
|
||||
const val DATABASE_VERSION = 253
|
||||
const val DATABASE_VERSION = 254
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
|
||||
/**
|
||||
* Adds a unique constraint to chat folder membership
|
||||
*/
|
||||
@Suppress("ClassName")
|
||||
object V254_AddChatFolderConstraint : SignalDatabaseMigration {
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL("DROP INDEX IF EXISTS chat_folder_membership_chat_folder_id_index")
|
||||
db.execSQL("DROP INDEX IF EXISTS chat_folder_membership_thread_id_index")
|
||||
db.execSQL("DROP INDEX IF EXISTS chat_folder_membership_membership_type_index")
|
||||
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE TABLE chat_folder_membership_tmp (
|
||||
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chat_folder_id INTEGER NOT NULL REFERENCES chat_folder (_id) ON DELETE CASCADE,
|
||||
thread_id INTEGER NOT NULL REFERENCES thread (_id) ON DELETE CASCADE,
|
||||
membership_type INTEGER DEFAULT 1,
|
||||
UNIQUE(chat_folder_id, thread_id) ON CONFLICT REPLACE
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
db.execSQL(
|
||||
"""
|
||||
INSERT INTO chat_folder_membership_tmp
|
||||
SELECT
|
||||
_id,
|
||||
chat_folder_id,
|
||||
thread_id,
|
||||
membership_type
|
||||
FROM chat_folder_membership
|
||||
"""
|
||||
)
|
||||
|
||||
db.execSQL("DROP TABLE chat_folder_membership")
|
||||
db.execSQL("ALTER TABLE chat_folder_membership_tmp RENAME TO chat_folder_membership")
|
||||
|
||||
db.execSQL("CREATE INDEX chat_folder_membership_thread_id_index ON chat_folder_membership (thread_id)")
|
||||
db.execSQL("CREATE INDEX chat_folder_membership_membership_type_index ON chat_folder_membership (membership_type)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user