feat: channel menu and mark as read

This commit is contained in:
Infi 2023-02-28 00:03:11 +01:00
parent b0579d1436
commit dd78679901
8 changed files with 493 additions and 296 deletions

View File

@ -0,0 +1,127 @@
package chat.revolt.components.screens.chat.drawer.channel
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import chat.revolt.R
import chat.revolt.api.RevoltAPI
import chat.revolt.api.schemas.ChannelType
import chat.revolt.components.screens.chat.DoubleDrawerState
import chat.revolt.components.screens.chat.drawer.server.DrawerChannel
import kotlinx.coroutines.launch
@Composable
fun RowScope.ChannelList(
serverId: String,
navController: NavController,
drawerState: DoubleDrawerState
) {
val coroutineScope = rememberCoroutineScope()
val navBackStackEntry by navController.currentBackStackEntryAsState()
Surface(
tonalElevation = 1.dp,
modifier = Modifier
.padding(start = 4.dp, top = 8.dp, bottom = 8.dp)
.clip(RoundedCornerShape(16.dp))
) {
Column(
Modifier
.weight(1f)
) {
if (serverId == "home") {
Column(
Modifier
.weight(1f)
.verticalScroll(rememberScrollState())
) {
RevoltAPI.channelCache.values.filter { it.channelType == ChannelType.Group }
.forEach { channel ->
DrawerChannel(
name = channel.name
?: "GDM #${channel.id}",
channelType = ChannelType.Group,
selected = (channel.id == navBackStackEntry?.arguments?.getString(
"channelId"
)),
hasUnread = channel.lastMessageID?.let { lastMessageID ->
RevoltAPI.unreads.hasUnread(
channel.id!!,
lastMessageID
)
} ?: false,
onClick = {
navController.navigate("channel/${channel.id}")
coroutineScope.launch { drawerState.focusCenter() }
},
onLongClick = {
navController.navigate("channel/${channel.id}/info")
}
)
}
}
} else {
val server = RevoltAPI.serverCache[serverId]
Text(
text = server?.name
?: stringResource(R.string.unknown),
style = MaterialTheme.typography.labelLarge,
fontSize = 24.sp,
modifier = Modifier.padding(16.dp)
)
Column(
Modifier
.weight(1f)
.verticalScroll(rememberScrollState())
) {
server?.channels?.forEach { channelId ->
RevoltAPI.channelCache[channelId]?.let { ch ->
DrawerChannel(
name = ch.name!!,
channelType = ch.channelType!!,
selected = navBackStackEntry?.arguments?.getString(
"channelId"
) == ch.id,
hasUnread = ch.lastMessageID?.let { lastMessageID ->
RevoltAPI.unreads.hasUnread(
ch.id!!,
lastMessageID
)
} ?: true,
onClick = {
coroutineScope.launch { drawerState.focusCenter() }
navController.navigate("channel/${ch.id}") {
popUpTo("home") {
inclusive = true
}
}
},
onLongClick = {
navController.navigate("channel/${ch.id}/menu")
}
)
}
}
}
}
}
}
}

View File

@ -3,8 +3,9 @@ package chat.revolt.components.screens.chat.drawer.server
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.LocalContentColor
@ -22,13 +23,15 @@ import androidx.compose.ui.unit.dp
import chat.revolt.api.schemas.ChannelType
import chat.revolt.components.screens.chat.ChannelIcon
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DrawerChannel(
channelType: ChannelType,
name: String,
selected: Boolean,
hasUnread: Boolean,
onClick: () -> Unit
onClick: () -> Unit,
onLongClick: () -> Unit = {},
) {
val backgroundColor = animateColorAsState(
if (selected) MaterialTheme.colorScheme.background
@ -52,7 +55,10 @@ fun DrawerChannel(
.clip(MaterialTheme.shapes.medium)
.background(backgroundColor.value)
.alpha(channelAlpha.value)
.clickable(onClick = onClick)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick
)
.padding(vertical = 8.dp, horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {

View File

@ -12,12 +12,10 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@ -30,17 +28,17 @@ import chat.revolt.R
import chat.revolt.api.RevoltAPI
import chat.revolt.api.realtime.DisconnectionState
import chat.revolt.api.realtime.RealtimeSocket
import chat.revolt.api.schemas.ChannelType
import chat.revolt.components.chat.DisconnectedNotice
import chat.revolt.components.generic.UserAvatar
import chat.revolt.components.generic.presenceFromStatus
import chat.revolt.components.screens.chat.DoubleDrawer
import chat.revolt.components.screens.chat.drawer.server.DrawerChannel
import chat.revolt.components.screens.chat.drawer.channel.ChannelList
import chat.revolt.components.screens.chat.drawer.server.DrawerServer
import chat.revolt.components.screens.chat.drawer.server.DrawerServerlikeIcon
import chat.revolt.components.screens.chat.drawer.server.ServerDrawerSeparator
import chat.revolt.components.screens.chat.rememberDoubleDrawerState
import chat.revolt.screens.chat.dialogs.safety.ReportMessageDialog
import chat.revolt.screens.chat.sheets.ChannelContextSheet
import chat.revolt.screens.chat.sheets.ChannelInfoSheet
import chat.revolt.screens.chat.sheets.MessageContextSheet
import chat.revolt.screens.chat.sheets.StatusSheet
@ -91,7 +89,6 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie
val bottomSheetNavigator = rememberBottomSheetNavigator()
val navController = rememberNavController(bottomSheetNavigator)
val navBackStackEntry by navController.currentBackStackEntryAsState()
ModalBottomSheetLayout(
sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
@ -176,91 +173,11 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie
}
Crossfade(targetState = viewModel.currentServer) {
Surface(
tonalElevation = 1.dp,
modifier = Modifier
.padding(start = 4.dp, top = 8.dp, bottom = 8.dp)
.clip(RoundedCornerShape(16.dp))
) {
Column(
Modifier
.weight(1f)
) {
if (it == "home") {
Column(
Modifier
.weight(1f)
.verticalScroll(rememberScrollState())
) {
RevoltAPI.channelCache.values.filter { it.channelType == ChannelType.Group }
.forEach { channel ->
DrawerChannel(
name = channel.name
?: "GDM #${channel.id}",
channelType = ChannelType.Group,
selected = channel.id == (navBackStackEntry?.arguments?.getString(
"channelId"
) ?: false),
hasUnread = channel.lastMessageID?.let { lastMessageID ->
RevoltAPI.unreads.hasUnread(
channel.id!!,
lastMessageID
)
} ?: false,
onClick = {
navController.navigate("channel/${channel.id}")
scope.launch {
drawerState.focusCenter()
}
}
)
}
}
} else {
val server = RevoltAPI.serverCache[it]
Text(
text = server?.name
?: stringResource(R.string.unknown),
style = MaterialTheme.typography.labelLarge,
fontSize = 24.sp,
modifier = Modifier.padding(16.dp)
)
Column(
Modifier
.weight(1f)
.verticalScroll(rememberScrollState())
) {
server?.channels?.forEach { channelId ->
RevoltAPI.channelCache[channelId]?.let { ch ->
DrawerChannel(
name = ch.name!!,
channelType = ch.channelType!!,
selected = navBackStackEntry?.arguments?.getString(
"channelId"
) == ch.id,
hasUnread = ch.lastMessageID?.let { lastMessageID ->
RevoltAPI.unreads.hasUnread(
ch.id!!,
lastMessageID
)
} ?: true,
onClick = {
scope.launch { drawerState.focusCenter() }
navController.navigate("channel/${ch.id}") {
popUpTo("home") {
inclusive = true
}
}
}
)
}
}
}
}
}
}
ChannelList(
serverId = it,
navController = navController,
drawerState = drawerState
)
}
}
}
@ -300,6 +217,15 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie
)
}
}
bottomSheet("channel/{channelId}/menu") { backStackEntry ->
val channelId = backStackEntry.arguments?.getString("channelId")
if (channelId != null) {
ChannelContextSheet(
navController = navController,
channelId = channelId
)
}
}
bottomSheet("message/{messageId}/menu") { backStackEntry ->
val messageId = backStackEntry.arguments?.getString("messageId")
if (messageId != null) {

View File

@ -0,0 +1,86 @@
package chat.revolt.screens.chat.sheets
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.navigation.NavController
import chat.revolt.R
import chat.revolt.api.RevoltAPI
import chat.revolt.components.generic.SheetClickable
import kotlinx.coroutines.launch
@Composable
fun ChannelContextSheet(
navController: NavController,
channelId: String,
) {
val channel = RevoltAPI.channelCache[channelId]
if (channel == null) {
navController.popBackStack()
return
}
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
Column {
SheetClickable(
icon = { modifier ->
Icon(
painter = painterResource(id = R.drawable.ic_content_copy_id_24dp),
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.channel_context_sheet_actions_copy_id),
style = style
)
},
) {
if (channel.id == null) return@SheetClickable
clipboardManager.setText(AnnotatedString(channel.id))
Toast.makeText(
context,
context.getString(R.string.channel_context_sheet_actions_copy_id_copied),
Toast.LENGTH_SHORT
).show()
navController.popBackStack()
}
SheetClickable(
icon = { modifier ->
Icon(
painter = painterResource(id = R.drawable.ic_eye_check_24dp),
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.channel_context_sheet_actions_mark_read),
style = style
)
},
) {
coroutineScope.launch {
channel.lastMessageID?.let {
RevoltAPI.unreads.markAsRead(channelId, it, sync = true)
}
}
navController.popBackStack()
}
}
}

View File

@ -8,7 +8,10 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.*
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -40,234 +43,256 @@ fun MessageContextSheet(
val context = LocalContext.current
val clipboardManager = LocalClipboardManager.current
Surface {
Column(
Column(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.verticalScroll(rememberScrollState()),
) {
Box(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.verticalScroll(rememberScrollState()),
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp))
.padding(bottom = 8.dp)
) {
Box(
modifier = Modifier
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp))
.padding(bottom = 8.dp)
) {
Message(
message = message.copy(
tail = false,
masquerade = null
),
truncate = true
Message(
message = message.copy(
tail = false,
masquerade = null
),
truncate = true
)
}
Spacer(modifier = Modifier.height(8.dp))
SheetClickable(
icon = { modifier ->
Icon(
painter = painterResource(id = R.drawable.ic_reply_24dp),
contentDescription = null,
modifier = modifier
)
}
},
label = { style ->
Text(
text = stringResource(id = R.string.message_context_sheet_actions_reply),
style = style
)
},
) {
UiCallbacks.emitQueueMessageForReply(messageId)
navController.popBackStack()
}
Spacer(modifier = Modifier.height(8.dp))
SheetClickable(
icon = { modifier ->
Icon(
painter = painterResource(id = R.drawable.ic_hamburger_plus_24dp),
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.message_context_sheet_actions_react),
style = style
)
},
) {
Toast.makeText(
context,
context.getString(R.string.comingsoon_toast),
Toast.LENGTH_SHORT
).show()
navController.popBackStack()
}
SheetClickable(
icon = { modifier ->
Icon(
painter = painterResource(id = R.drawable.ic_reply_24dp),
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.message_context_sheet_actions_reply),
style = style
)
},
) {
UiCallbacks.emitQueueMessageForReply(messageId)
navController.popBackStack()
}
SheetClickable(
icon = { modifier ->
Icon(
painter = painterResource(id = R.drawable.ic_hamburger_plus_24dp),
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.message_context_sheet_actions_react),
style = style
)
},
) {
SheetClickable(
icon = { modifier ->
Icon(
painter = painterResource(id = R.drawable.ic_content_copy_24dp),
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.message_context_sheet_actions_copy),
style = style
)
},
) {
if (message.content == null || message.content.isEmpty()) {
Toast.makeText(
context,
context.getString(R.string.comingsoon_toast),
context.getString(R.string.message_context_sheet_actions_copy_failed_empty),
Toast.LENGTH_SHORT
).show()
navController.popBackStack()
return@SheetClickable
}
SheetClickable(
icon = { modifier ->
Icon(
painter = painterResource(id = R.drawable.ic_content_copy_24dp),
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.message_context_sheet_actions_copy),
style = style
)
},
) {
if (message.content == null || message.content.isEmpty()) {
Toast.makeText(
context,
context.getString(R.string.message_context_sheet_actions_copy_failed_empty),
Toast.LENGTH_SHORT
).show()
navController.popBackStack()
return@SheetClickable
}
clipboardManager.setText(AnnotatedString(message.content))
Toast.makeText(
context,
context.getString(R.string.copied),
Toast.LENGTH_SHORT
).show()
navController.popBackStack()
}
clipboardManager.setText(AnnotatedString(message.content))
SheetClickable(
icon = { modifier ->
Icon(
painter = painterResource(id = R.drawable.ic_link_variant_24dp),
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.message_context_sheet_actions_copy_link),
style = style
)
},
) {
if (message.content == null || message.content.isEmpty()) {
Toast.makeText(
context,
context.getString(R.string.copied),
context.getString(R.string.message_context_sheet_actions_copy_failed_empty),
Toast.LENGTH_SHORT
).show()
navController.popBackStack()
return@SheetClickable
}
SheetClickable(
icon = { modifier ->
Icon(
painter = painterResource(id = R.drawable.ic_link_variant_24dp),
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.message_context_sheet_actions_copy_link),
style = style
)
},
) {
if (message.content == null || message.content.isEmpty()) {
Toast.makeText(
context,
context.getString(R.string.message_context_sheet_actions_copy_failed_empty),
Toast.LENGTH_SHORT
).show()
navController.popBackStack()
return@SheetClickable
}
val server = RevoltAPI.serverCache.values.find { server ->
server.channels?.contains(message.channel) ?: false
}
val messageLink =
"$REVOLT_APP/server/${server?.id}/channel/${message.channel}/${message.id}"
clipboardManager.setText(AnnotatedString(messageLink))
Toast.makeText(
context,
context.getString(R.string.message_context_sheet_actions_copy_link_copied),
Toast.LENGTH_SHORT
).show()
navController.popBackStack()
val server = RevoltAPI.serverCache.values.find { server ->
server.channels?.contains(message.channel) ?: false
}
val messageLink =
"$REVOLT_APP/server/${server?.id}/channel/${message.channel}/${message.id}"
SheetClickable(
icon = { modifier ->
Icon(
painter = painterResource(id = R.drawable.ic_content_copy_id_24dp),
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.message_context_sheet_actions_copy_id),
style = style
)
},
) {
if (message.id == null) return@SheetClickable
clipboardManager.setText(AnnotatedString(messageLink))
Toast.makeText(
context,
context.getString(R.string.message_context_sheet_actions_copy_link_copied),
Toast.LENGTH_SHORT
).show()
navController.popBackStack()
}
clipboardManager.setText(AnnotatedString(message.id))
Toast.makeText(
context,
context.getString(R.string.message_context_sheet_actions_copy_id_copied),
Toast.LENGTH_SHORT
).show()
navController.popBackStack()
}
SheetClickable(
icon = { modifier ->
Icon(
painter = painterResource(id = R.drawable.ic_content_copy_id_24dp),
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.message_context_sheet_actions_copy_id),
style = style
)
},
) {
if (message.id == null) return@SheetClickable
SheetClickable(
icon = { modifier ->
Icon(
imageVector = Icons.Default.Edit,
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.message_context_sheet_actions_edit),
style = style
)
},
) {
Toast.makeText(
context,
context.getString(R.string.comingsoon_toast),
Toast.LENGTH_SHORT
).show()
navController.popBackStack()
}
clipboardManager.setText(AnnotatedString(message.id))
Toast.makeText(
context,
context.getString(R.string.message_context_sheet_actions_copy_id_copied),
Toast.LENGTH_SHORT
).show()
navController.popBackStack()
}
SheetClickable(
icon = { modifier ->
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.message_context_sheet_actions_delete),
style = style
)
},
) {
Toast.makeText(
context,
context.getString(R.string.comingsoon_toast),
Toast.LENGTH_SHORT
).show()
navController.popBackStack()
}
SheetClickable(
icon = { modifier ->
Icon(
painter = painterResource(id = R.drawable.ic_flag_24dp),
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.message_context_sheet_actions_report),
style = style
)
},
) {
navController.navigate("report/message/${message.id}")
}
SheetClickable(
icon = { modifier ->
Icon(
painter = painterResource(id = R.drawable.ic_eye_off_24dp),
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.message_context_sheet_actions_mark_unread),
style = style
)
},
) {
Toast.makeText(
context,
context.getString(R.string.comingsoon_toast),
Toast.LENGTH_SHORT
).show()
navController.popBackStack()
}
SheetClickable(
icon = { modifier ->
Icon(
imageVector = Icons.Default.Edit,
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.message_context_sheet_actions_edit),
style = style
)
},
) {
Toast.makeText(
context,
context.getString(R.string.comingsoon_toast),
Toast.LENGTH_SHORT
).show()
navController.popBackStack()
}
SheetClickable(
icon = { modifier ->
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.message_context_sheet_actions_delete),
style = style
)
},
) {
Toast.makeText(
context,
context.getString(R.string.comingsoon_toast),
Toast.LENGTH_SHORT
).show()
navController.popBackStack()
}
SheetClickable(
icon = { modifier ->
Icon(
painter = painterResource(id = R.drawable.ic_flag_24dp),
contentDescription = null,
modifier = modifier
)
},
label = { style ->
Text(
text = stringResource(id = R.string.message_context_sheet_actions_report),
style = style
)
},
) {
navController.navigate("report/message/${message.id}")
}
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M23.5,17L18.5,22L15,18.5L16.5,17L18.5,19L22,15.5L23.5,17M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9M12,17C12.5,17 12.97,16.93 13.42,16.79C13.15,17.5 13,18.22 13,19V19.45L12,19.5C7,19.5 2.73,16.39 1,12C2.73,7.61 7,4.5 12,4.5C17,4.5 21.27,7.61 23,12C22.75,12.64 22.44,13.26 22.08,13.85C21.18,13.31 20.12,13 19,13C18.22,13 17.5,13.15 16.79,13.42C16.93,12.97 17,12.5 17,12A5,5 0 0,0 12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17Z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M11.83,9L15,12.16C15,12.11 15,12.05 15,12A3,3 0 0,0 12,9C11.94,9 11.89,9 11.83,9M7.53,9.8L9.08,11.35C9.03,11.56 9,11.77 9,12A3,3 0 0,0 12,15C12.22,15 12.44,14.97 12.65,14.92L14.2,16.47C13.53,16.8 12.79,17 12,17A5,5 0 0,1 7,12C7,11.21 7.2,10.47 7.53,9.8M2,4.27L4.28,6.55L4.73,7C3.08,8.3 1.78,10 1,12C2.73,16.39 7,19.5 12,19.5C13.55,19.5 15.03,19.2 16.38,18.66L16.81,19.08L19.73,22L21,20.73L3.27,3M12,7A5,5 0 0,1 17,12C17,12.64 16.87,13.26 16.64,13.82L19.57,16.75C21.07,15.5 22.27,13.86 23,12C21.27,7.61 17,4.5 12,4.5C10.6,4.5 9.26,4.75 8,5.2L10.17,7.35C10.74,7.13 11.35,7 12,7Z" />
</vector>

View File

@ -140,6 +140,15 @@
<string name="message_context_sheet_actions_copy_link_copied">Copied message link to clipboard</string>
<string name="message_context_sheet_actions_react">React</string>
<string name="message_context_sheet_actions_report">Report</string>
<string name="message_context_sheet_actions_mark_unread">Mark as unread</string>
<string name="channel_context_sheet_actions_copy_id">Copy ID</string>
<string name="channel_context_sheet_actions_copy_id_copied">Copied channel ID to clipboard</string>
<string name="channel_context_sheet_actions_mark_read">Mark as read</string>
<string name="server_context_sheet_actions_copy_id">Copy ID</string>
<string name="server_context_sheet_actions_copy_id_copied">Copied server ID to clipboard</string>
<string name="server_context_sheet_actions_mark_read">Mark as read</string>
<string name="report">Report</string>
<string name="report_cancel">Cancel</string>