Allow adding and removing from context menu.

This commit is contained in:
Michelle Tang 2024-10-16 13:57:49 -07:00 committed by Greyson Parrelli
parent 94d6bfd9ad
commit 6fcfd8fdb1
13 changed files with 330 additions and 7 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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="M19,11C18.314,11 17.67,10.87 17.068,10.612C16.466,10.353 15.935,9.994 15.476,9.534C15.016,9.068 14.654,8.534 14.388,7.932C14.129,7.33 14,6.686 14,6C14,5.314 14.129,4.67 14.388,4.068C14.654,3.466 15.016,2.935 15.476,2.476C15.935,2.01 16.466,1.647 17.068,1.388C17.67,1.129 18.314,1 19,1C19.686,1 20.33,1.129 20.932,1.388C21.534,1.647 22.065,2.006 22.524,2.466C22.984,2.926 23.343,3.46 23.602,4.068C23.867,4.67 24,5.314 24,6C24,6.68 23.867,7.324 23.602,7.932C23.343,8.534 22.981,9.065 22.515,9.524C22.055,9.984 21.521,10.346 20.913,10.612C20.311,10.87 19.673,11 19,11ZM19,9.165C19.201,9.165 19.359,9.104 19.476,8.981C19.599,8.858 19.66,8.699 19.66,8.505V6.66H21.505C21.699,6.66 21.858,6.602 21.981,6.485C22.104,6.362 22.165,6.201 22.165,6C22.165,5.799 22.104,5.641 21.981,5.524C21.858,5.401 21.699,5.34 21.505,5.34H19.66V3.495C19.66,3.301 19.599,3.142 19.476,3.019C19.359,2.896 19.201,2.835 19,2.835C18.799,2.835 18.638,2.896 18.515,3.019C18.398,3.142 18.34,3.301 18.34,3.495V5.34H16.495C16.301,5.34 16.142,5.401 16.019,5.524C15.896,5.641 15.835,5.799 15.835,6C15.835,6.201 15.896,6.362 16.019,6.485C16.142,6.602 16.301,6.66 16.495,6.66H18.34V8.505C18.34,8.699 18.398,8.858 18.515,8.981C18.638,9.104 18.799,9.165 19,9.165Z"
android:fillColor="#000000"/>
</vector>

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="M19,11C18.314,11 17.67,10.871 17.068,10.612C16.466,10.353 15.935,9.994 15.476,9.534C15.016,9.068 14.654,8.534 14.388,7.932C14.129,7.33 14,6.686 14,6C14,5.314 14.129,4.67 14.388,4.068C14.654,3.466 15.016,2.935 15.476,2.476C15.935,2.01 16.466,1.647 17.068,1.388C17.67,1.129 18.314,1 19,1C19.686,1 20.33,1.129 20.932,1.388C21.534,1.647 22.065,2.006 22.524,2.466C22.984,2.926 23.343,3.46 23.602,4.068C23.867,4.67 24,5.314 24,6C24,6.68 23.867,7.324 23.602,7.932C23.343,8.534 22.981,9.065 22.515,9.524C22.055,9.984 21.521,10.346 20.913,10.612C20.311,10.871 19.673,11 19,11ZM16.485,6.66H21.495C21.676,6.66 21.832,6.595 21.961,6.466C22.097,6.33 22.165,6.175 22.165,6C22.165,5.825 22.097,5.673 21.961,5.544C21.832,5.408 21.676,5.34 21.495,5.34H16.485C16.304,5.34 16.149,5.408 16.019,5.544C15.89,5.673 15.825,5.825 15.825,6C15.825,6.175 15.89,6.33 16.019,6.466C16.149,6.595 16.304,6.66 16.485,6.66Z"
android:fillColor="#000000"/>
</vector>

View file

@ -740,6 +740,15 @@
<item quantity="one">%d selected</item>
<item quantity="other">%d selected</item>
</plurals>
<!-- Context menu option to add a chat to a folder -->
<string name="ConversationListFragment_add_to_folder">Add to folder</string>
<!-- Context menu option to remove a chat from a folder -->
<string name="ConversationListFragment_remove_from_folder">Remove from folder</string>
<!-- Bottom sheet title when choosing a folder to add a chat to -->
<string name="AddToFolderBottomSheet_choose_a_folder">Choose a folder</string>
<!-- Toast shown when a chat has been added to a folder, where %s is the name of the folder -->
<string name="AddToFolderBottomSheet_added_to_s">Added to \"%1$s\"</string>
<!-- Show in conversation list overflow menu to open selection bottom sheet -->
<string name="ConversationListFragment__notification_profile">Notification profile</string>
@ -5117,6 +5126,8 @@
<item quantity="one">%1$d chat excluded</item>
<item quantity="other">%1$d chats excluded</item>
</plurals>
<!-- Badge shown in chat folder tab when unread count is greater than 99 -->
<string name="ChatFolderAdapter__99p">99+</string>
<!-- CreateFoldersFragment -->
<!-- Title of the screen when creating a folder, displayed in the toolbar -->