diff --git a/app/src/main/java/chat/revolt/api/RevoltAPI.kt b/app/src/main/java/chat/revolt/api/RevoltAPI.kt index 8d72f6ea..19dd844a 100644 --- a/app/src/main/java/chat/revolt/api/RevoltAPI.kt +++ b/app/src/main/java/chat/revolt/api/RevoltAPI.kt @@ -24,6 +24,7 @@ const val REVOLT_SUPPORT = "https://support.revolt.chat" const val REVOLT_MARKETING = "https://revolt.chat" const val REVOLT_FILES = "https://autumn.revolt.chat" const val REVOLT_JANUARY = "https://jan.revolt.chat" +const val REVOLT_APP = "https://app.revolt.chat" const val REVOLT_WEBSOCKET = "wss://ws.revolt.chat" fun asJanuaryProxyUrl(url: String): String { diff --git a/app/src/main/java/chat/revolt/callbacks/UiCallbacks.kt b/app/src/main/java/chat/revolt/callbacks/UiCallbacks.kt new file mode 100644 index 00000000..2c2b7cf2 --- /dev/null +++ b/app/src/main/java/chat/revolt/callbacks/UiCallbacks.kt @@ -0,0 +1,28 @@ +package chat.revolt.callbacks + +/** + * Callbacks for UI events, such as when a user selects "reply" on a message, so that the + * channel screen can add a reply to the message, for example. + * + * We do this by having a singleton object that contains all the receivers, and then + * the UI can set the callbacks to whatever it wants. + */ +object UiCallbacks { + interface CallbackReceiver { + fun onQueueMessageForReply(messageId: String) + } + + var receivers = mutableListOf() + + fun registerReceiver(receiver: CallbackReceiver) { + receivers.add(receiver) + } + + fun unregisterReceiver(receiver: CallbackReceiver) { + receivers.remove(receiver) + } + + fun emitQueueMessageForReply(messageId: String) { + receivers.forEach { it.onQueueMessageForReply(messageId) } + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/components/chat/Message.kt b/app/src/main/java/chat/revolt/components/chat/Message.kt index 2adfcee5..1edeac16 100644 --- a/app/src/main/java/chat/revolt/components/chat/Message.kt +++ b/app/src/main/java/chat/revolt/components/chat/Message.kt @@ -1,7 +1,6 @@ package chat.revolt.components.chat import android.net.Uri -import android.widget.Toast import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.foundation.* import androidx.compose.foundation.layout.* @@ -11,14 +10,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.revolt.R import chat.revolt.api.REVOLT_FILES import chat.revolt.api.RevoltAPI import chat.revolt.api.asJanuaryProxyUrl @@ -53,11 +49,12 @@ fun formatLongAsTime(time: Long): String { @OptIn(ExperimentalFoundationApi::class) @Composable fun Message( - message: MessageSchema + message: MessageSchema, + truncate: Boolean = false, + onMessageContextMenu: () -> Unit = {}, ) { val author = RevoltAPI.userCache[message.author] ?: return CircularProgressIndicator() val context = LocalContext.current - val clipboardManager = LocalClipboardManager.current Column { if (message.tail == false) { @@ -80,17 +77,7 @@ fun Message( .combinedClickable( onClick = {}, onLongClick = { - if (message.content != null && message.content.isNotEmpty()) { - clipboardManager.setText(AnnotatedString(message.content)) - - Toast - .makeText( - context, - context.getString(R.string.copied), - Toast.LENGTH_SHORT - ) - .show() - } + onMessageContextMenu() } ) .padding(horizontal = 10.dp) @@ -145,6 +132,8 @@ fun Message( message.content?.let { Text( text = Renderer.annotateMarkdown(it), + maxLines = if (truncate) 1 else Int.MAX_VALUE, + overflow = TextOverflow.Ellipsis ) } diff --git a/app/src/main/java/chat/revolt/components/screens/settings/SettingsCategory.kt b/app/src/main/java/chat/revolt/components/generic/SheetClickable.kt similarity index 89% rename from app/src/main/java/chat/revolt/components/screens/settings/SettingsCategory.kt rename to app/src/main/java/chat/revolt/components/generic/SheetClickable.kt index 24691ccb..0e771b3d 100644 --- a/app/src/main/java/chat/revolt/components/screens/settings/SettingsCategory.kt +++ b/app/src/main/java/chat/revolt/components/generic/SheetClickable.kt @@ -1,4 +1,4 @@ -package chat.revolt.components.screens.settings +package chat.revolt.components.generic import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -21,7 +21,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Composable -fun SettingsCategory( +fun SheetClickable( icon: @Composable (Modifier) -> Unit, label: @Composable (TextStyle) -> Unit, onClick: () -> Unit, @@ -30,7 +30,7 @@ fun SettingsCategory( Row( modifier = Modifier .clip(MaterialTheme.shapes.medium) - .background(MaterialTheme.colorScheme.surface) + .background(MaterialTheme.colorScheme.background) .clickable(onClick = onClick) .padding(all = 4.dp) .fillMaxWidth() @@ -40,7 +40,7 @@ fun SettingsCategory( icon(Modifier.padding(end = 16.dp)) label( MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.onSurface, + color = MaterialTheme.colorScheme.onBackground, fontWeight = FontWeight.SemiBold, ) ) @@ -51,7 +51,7 @@ fun SettingsCategory( @Preview @Composable fun SettingsCategoryPreview() { - SettingsCategory( + SheetClickable( icon = { modifier -> Icon( modifier = modifier, diff --git a/app/src/main/java/chat/revolt/components/screens/chat/ReplyManager.kt b/app/src/main/java/chat/revolt/components/screens/chat/ReplyManager.kt new file mode 100644 index 00000000..bb115d62 --- /dev/null +++ b/app/src/main/java/chat/revolt/components/screens/chat/ReplyManager.kt @@ -0,0 +1,69 @@ +package chat.revolt.components.screens.chat + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import chat.revolt.api.routes.channel.SendMessageReply +import chat.revolt.components.chat.InReplyTo + +@Composable +fun ManageableReply( + reply: SendMessageReply, + onToggleMention: () -> Unit, + onRemove: () -> Unit, +) { + // TODO Revamp this. Placeholder design ("functional" but extremely ugly) + Row( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)) + .horizontalScroll(rememberScrollState()) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Remove reply", + modifier = Modifier + .clickable { + onRemove() + } + ) + InReplyTo( + messageId = reply.id, + withMention = reply.mention, + modifier = Modifier.weight(1f) + ) { + onToggleMention() + } + } +} + +@Composable +fun ReplyManager( + replies: List, + onToggleMention: (SendMessageReply) -> Unit, + onRemove: (SendMessageReply) -> Unit, +) { + Column { + replies.forEach { reply -> + ManageableReply( + reply = reply, + onToggleMention = { onToggleMention(reply) }, + onRemove = { onRemove(reply) } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt index 5961d861..3334fb8e 100644 --- a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt @@ -33,6 +33,7 @@ import chat.revolt.api.schemas.ChannelType import chat.revolt.components.chat.DisconnectedNotice import chat.revolt.components.screens.chat.* import chat.revolt.screens.chat.sheets.ChannelInfoSheet +import chat.revolt.screens.chat.sheets.MessageContextSheet import chat.revolt.screens.chat.views.ChannelScreen import chat.revolt.screens.chat.views.HomeScreen import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi @@ -228,6 +229,7 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie ) } } + bottomSheet("channel/{channelId}/info") { backStackEntry -> val channelId = backStackEntry.arguments?.getString("channelId") if (channelId != null) { @@ -237,6 +239,15 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie ) } } + bottomSheet("message/{messageId}/menu") { backStackEntry -> + val messageId = backStackEntry.arguments?.getString("messageId") + if (messageId != null) { + MessageContextSheet( + navController = navController, + messageId = messageId + ) + } + } } } } diff --git a/app/src/main/java/chat/revolt/screens/chat/sheets/MessageContextSheet.kt b/app/src/main/java/chat/revolt/screens/chat/sheets/MessageContextSheet.kt new file mode 100644 index 00000000..92f6a866 --- /dev/null +++ b/app/src/main/java/chat/revolt/screens/chat/sheets/MessageContextSheet.kt @@ -0,0 +1,276 @@ +package chat.revolt.screens.chat.sheets + +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +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.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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.compose.ui.unit.dp +import androidx.navigation.NavController +import chat.revolt.R +import chat.revolt.api.REVOLT_APP +import chat.revolt.api.RevoltAPI +import chat.revolt.callbacks.UiCallbacks +import chat.revolt.components.chat.Message +import chat.revolt.components.generic.SheetClickable +import chat.revolt.api.schemas.Message as MessageSchema + +@Composable +fun MessageContextSheet( + navController: NavController, + messageId: String, +) { + val message = RevoltAPI.messageCache[messageId] + if (message == null) { + navController.popBackStack() + return + } + + val context = LocalContext.current + val clipboardManager = LocalClipboardManager.current + + Surface { + Column( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .verticalScroll(rememberScrollState()), + ) { + Box( + modifier = Modifier + .clip(MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)) + .padding(bottom = 8.dp) + ) { + Message( + message = message.mergeWithPartial(MessageSchema(tail = false)), + 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() + } + + 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_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() + } + + 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() + } + + 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(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.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 + ) + }, + ) { + Toast.makeText( + context, + context.getString(R.string.comingsoon_toast), + Toast.LENGTH_SHORT + ).show() + navController.popBackStack() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/screens/chat/views/ChannelScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/ChannelScreen.kt index d87bc619..a36abbc5 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/ChannelScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/ChannelScreen.kt @@ -36,6 +36,7 @@ import chat.revolt.api.realtime.RealtimeSocket import chat.revolt.api.realtime.frames.receivable.ChannelStartTypingFrame import chat.revolt.api.realtime.frames.receivable.ChannelStopTypingFrame import chat.revolt.api.realtime.frames.receivable.MessageFrame +import chat.revolt.api.routes.channel.SendMessageReply import chat.revolt.api.routes.channel.fetchMessagesFromChannel import chat.revolt.api.routes.channel.sendMessage import chat.revolt.api.routes.microservices.autumn.FileArgs @@ -43,10 +44,12 @@ import chat.revolt.api.routes.microservices.autumn.MAX_ATTACHMENTS_PER_MESSAGE import chat.revolt.api.routes.microservices.autumn.uploadToAutumn import chat.revolt.api.routes.user.addUserIfUnknown import chat.revolt.api.schemas.Channel +import chat.revolt.callbacks.UiCallbacks import chat.revolt.components.chat.Message import chat.revolt.components.chat.MessageField import chat.revolt.components.screens.chat.AttachmentManager import chat.revolt.components.screens.chat.ChannelIcon +import chat.revolt.components.screens.chat.ReplyManager import chat.revolt.components.screens.chat.TypingIndicator import io.ktor.http.* import kotlinx.coroutines.flow.distinctUntilChanged @@ -60,9 +63,9 @@ class ChannelScreenViewModel : ViewModel() { val channel: Channel? get() = _channel - private var _callback = mutableStateOf(null) - private val callback: RealtimeSocket.ChannelCallback? - get() = _callback.value + private var _channelCallback = mutableStateOf(null) + private val channelCallback: RealtimeSocket.ChannelCallback? + get() = _channelCallback.value private var _renderableMessages = mutableStateListOf() val renderableMessages: List @@ -114,6 +117,36 @@ class ChannelScreenViewModel : ViewModel() { _sendingMessage = sending } + private var _replies = mutableStateListOf() + val replies: List + get() = _replies + + fun addInReplyTo(reply: SendMessageReply) { + _replies.add(reply) + } + + fun removeReply(reply: SendMessageReply) { + _replies.remove(reply) + } + + fun toggleReplyMentionFor(reply: SendMessageReply) { + val index = _replies.indexOf(reply) + val newReply = SendMessageReply( + reply.id, + !reply.mention + ) + + _replies[index] = newReply + } + + private fun clearInReplyTo() { + _replies.clear() + } + + private var _uiCallbackReceiver = mutableStateOf(null) + val uiCallbackReceiver: UiCallbacks.CallbackReceiver? + get() = _uiCallbackReceiver.value + inner class ChannelScreenCallback : RealtimeSocket.ChannelCallback { override fun onMessage(message: MessageFrame) { viewModelScope.launch { @@ -145,9 +178,20 @@ class ChannelScreenViewModel : ViewModel() { } } - private fun registerCallback() { - _callback.value = ChannelScreenCallback() - RealtimeSocket.registerChannelCallback(channel!!.id!!, callback!!) + inner class UiCallbackReceiver : UiCallbacks.CallbackReceiver { + override fun onQueueMessageForReply(messageId: String) { + viewModelScope.launch { + addInReplyTo(SendMessageReply(messageId, true)) + } + } + } + + private fun registerCallbacks() { + _channelCallback.value = ChannelScreenCallback() + RealtimeSocket.registerChannelCallback(channel!!.id!!, channelCallback!!) + + _uiCallbackReceiver.value = UiCallbackReceiver() + UiCallbacks.registerReceiver(uiCallbackReceiver!!) } fun fetchMessages() { @@ -218,7 +262,7 @@ class ChannelScreenViewModel : ViewModel() { Log.e("ChannelScreen", "Channel $id not in cache, for now this is fatal!") // FIXME } - registerCallback() + registerCallbacks() } fun sendPendingMessage() { @@ -246,11 +290,13 @@ class ChannelScreenViewModel : ViewModel() { sendMessage( channel!!.id!!, messageContent, - attachments = if (attachmentIds.isEmpty()) null else attachmentIds + attachments = if (attachmentIds.isEmpty()) null else attachmentIds, + replies = replies ) _messageContent = "" popAttachmentBatch() + clearInReplyTo() setSendingMessage(false) } } @@ -340,6 +386,7 @@ fun ChannelScreen( DisposableEffect(channelId) { onDispose { RealtimeSocket.unregisterChannelCallback(channelId) + viewModel.uiCallbackReceiver?.let { UiCallbacks.unregisterReceiver(it) } } } @@ -411,7 +458,9 @@ fun ChannelScreen( } items(viewModel.renderableMessages) { message -> - Message(message) + Message(message) { + navController.navigate("message/${message.id}/menu") + } } item { @@ -466,6 +515,14 @@ fun ChannelScreen( ) } + AnimatedVisibility(visible = viewModel.replies.isNotEmpty()) { + ReplyManager( + replies = viewModel.replies, + onRemove = viewModel::removeReply, + onToggleMention = viewModel::toggleReplyMentionFor + ) + } + AnimatedVisibility(visible = viewModel.attachments.isNotEmpty()) { AttachmentManager( attachments = viewModel.attachments, diff --git a/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt index 115ce6b6..6fd9c315 100644 --- a/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import chat.revolt.R import chat.revolt.components.generic.PageHeader -import chat.revolt.components.screens.settings.SettingsCategory +import chat.revolt.components.generic.SheetClickable @Composable fun SettingsScreen( @@ -37,7 +37,7 @@ fun SettingsScreen( .fillMaxSize() .padding(10.dp) ) { - SettingsCategory( + SheetClickable( icon = { modifier -> Icon( painter = painterResource(id = R.drawable.ic_palette_24dp), @@ -56,7 +56,7 @@ fun SettingsScreen( navController.navigate("settings/appearance") } - SettingsCategory( + SheetClickable( icon = { modifier -> Icon( imageVector = Icons.Default.Info, diff --git a/app/src/main/res/drawable/ic_content_copy_24dp.xml b/app/src/main/res/drawable/ic_content_copy_24dp.xml new file mode 100644 index 00000000..ec58dbcb --- /dev/null +++ b/app/src/main/res/drawable/ic_content_copy_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_content_copy_id_24dp.xml b/app/src/main/res/drawable/ic_content_copy_id_24dp.xml new file mode 100644 index 00000000..3b710af0 --- /dev/null +++ b/app/src/main/res/drawable/ic_content_copy_id_24dp.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_flag_24dp.xml b/app/src/main/res/drawable/ic_flag_24dp.xml new file mode 100644 index 00000000..4747b9b2 --- /dev/null +++ b/app/src/main/res/drawable/ic_flag_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_hamburger_plus_24dp.xml b/app/src/main/res/drawable/ic_hamburger_plus_24dp.xml new file mode 100644 index 00000000..aa97537b --- /dev/null +++ b/app/src/main/res/drawable/ic_hamburger_plus_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_reply_24dp.xml b/app/src/main/res/drawable/ic_reply_24dp.xml new file mode 100644 index 00000000..87e9b253 --- /dev/null +++ b/app/src/main/res/drawable/ic_reply_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 457c2d48..c4a4e659 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -61,6 +61,7 @@ Gah, you found me! The feature you are trying to access is not ready yet, but we are steadily working on polishing it to perfection.. + Sorry, this feature is not ready yet. %1$s is typing… @@ -116,6 +117,21 @@ Channel description There hasn\'t been a description set for this channel yet. + Copy + Message is empty, nothing to copy + Reply + Edit + Delete + Are you sure you want to delete this message? + Yes + No + Copy ID + Copied message ID to clipboard + Copy link + Copied message link to clipboard + React + Report + Appearance Theme System