feat: make revolt a share target (rough around edges)
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
35dc87ed7e
commit
1bf9610d7d
|
|
@ -86,6 +86,22 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activities.ShareTargetActivity"
|
||||
android:theme="@style/Theme.Revolt"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activities.media.ImageViewActivity"
|
||||
android:theme="@style/Theme.Revolt" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,461 @@
|
|||
package chat.revolt.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
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
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import chat.revolt.R
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.internals.ChannelUtils
|
||||
import chat.revolt.api.routes.channel.sendMessage
|
||||
import chat.revolt.api.routes.microservices.autumn.FileArgs
|
||||
import chat.revolt.api.routes.microservices.autumn.MAX_ATTACHMENTS_PER_MESSAGE
|
||||
import chat.revolt.api.routes.microservices.autumn.uploadToAutumn
|
||||
import chat.revolt.api.schemas.ChannelType
|
||||
import chat.revolt.api.settings.GlobalState
|
||||
import chat.revolt.api.settings.SyncedSettings
|
||||
import chat.revolt.components.chat.NativeMessageField
|
||||
import chat.revolt.components.emoji.EmojiPicker
|
||||
import chat.revolt.components.generic.presenceFromStatus
|
||||
import chat.revolt.components.screens.chat.AttachmentManager
|
||||
import chat.revolt.components.screens.chat.drawer.server.DrawerChannel
|
||||
import chat.revolt.components.screens.chat.drawer.server.DrawerChannelIconType
|
||||
import chat.revolt.persistence.KVStorage
|
||||
import chat.revolt.screens.chat.views.channel.BottomPane
|
||||
import chat.revolt.ui.theme.RevoltTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import io.ktor.http.ContentType
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ShareTargetActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val text: String? = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
val media: List<Uri?> = when (intent?.action) {
|
||||
// We receive one of something. Could be text, could be media.
|
||||
Intent.ACTION_SEND -> {
|
||||
when {
|
||||
// No media if we receive text/plain
|
||||
intent.type == "text/plain" -> {
|
||||
listOf()
|
||||
}
|
||||
|
||||
// Otherwise, we receive a single Uri
|
||||
else -> {
|
||||
listOf(
|
||||
when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> {
|
||||
intent.getParcelableExtra(
|
||||
Intent.EXTRA_STREAM,
|
||||
Parcelable::class.java
|
||||
) as? Uri
|
||||
}
|
||||
|
||||
else -> {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra(Intent.EXTRA_STREAM)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We receive multiple URIs, definitely media
|
||||
Intent.ACTION_SEND_MULTIPLE -> {
|
||||
try {
|
||||
val bundle: ArrayList<Uri>? = when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
intent.getParcelableArrayListExtra(
|
||||
Intent.EXTRA_STREAM,
|
||||
Parcelable::class.java
|
||||
) as? ArrayList<Uri>
|
||||
}
|
||||
|
||||
else -> {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
|
||||
}
|
||||
}
|
||||
|
||||
bundle ?: listOf()
|
||||
} catch (e: Exception) {
|
||||
Log.e("ShareTargetActivity", "Failed to get multiple URIs", e)
|
||||
listOf()
|
||||
}
|
||||
}
|
||||
|
||||
// We don't know what we're receiving
|
||||
else -> {
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(R.string.share_target_invalid_intent),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
|
||||
finish()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
setContent {
|
||||
ShareTargetScreen(
|
||||
text = text,
|
||||
media = media.filterNotNull(),
|
||||
onFinished = { finish() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@HiltViewModel
|
||||
class ShareTargetScreenViewModel @Inject constructor(
|
||||
private val kvStorage: KVStorage,
|
||||
) : ViewModel() {
|
||||
var apiIsReady by mutableStateOf(false)
|
||||
|
||||
var messageContent by mutableStateOf("")
|
||||
var attachments = mutableStateListOf<FileArgs>()
|
||||
var attachmentsUploading by mutableStateOf(false)
|
||||
var attachmentProgress by mutableFloatStateOf(0f)
|
||||
var activeBottomPane by mutableStateOf<BottomPane>(BottomPane.None)
|
||||
|
||||
suspend fun isLoggedIn(): Boolean {
|
||||
return kvStorage.get("sessionToken") != null
|
||||
}
|
||||
|
||||
suspend fun initialiseAPI() {
|
||||
if (!RevoltAPI.isLoggedIn()) {
|
||||
val token = kvStorage.get("sessionToken") ?: return
|
||||
RevoltAPI.loginAs(token)
|
||||
RevoltAPI.initialize()
|
||||
}
|
||||
apiIsReady = true
|
||||
}
|
||||
|
||||
fun send(channelId: String, onFinished: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
val attachmentIds = arrayListOf<String>()
|
||||
val takenAttachments = attachments.take(MAX_ATTACHMENTS_PER_MESSAGE)
|
||||
val totalTaken = takenAttachments.size
|
||||
|
||||
takenAttachments.forEachIndexed { index, it ->
|
||||
try {
|
||||
val id = uploadToAutumn(
|
||||
it.file,
|
||||
it.filename,
|
||||
"attachments",
|
||||
ContentType.parse(it.contentType),
|
||||
onProgress = { current, total ->
|
||||
attachmentProgress =
|
||||
((current.toFloat() / total.toFloat()) / totalTaken.toFloat()) + (index.toFloat() / totalTaken.toFloat())
|
||||
}
|
||||
)
|
||||
attachmentIds.add(id)
|
||||
} catch (e: Exception) {
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(
|
||||
channelId = channelId,
|
||||
content = messageContent,
|
||||
attachments = attachmentIds
|
||||
)
|
||||
|
||||
onFinished()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ShareTargetScreen(
|
||||
text: String?,
|
||||
media: List<Uri>?,
|
||||
onFinished: () -> Unit = {},
|
||||
viewModel: ShareTargetScreenViewModel = hiltViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (!viewModel.isLoggedIn()) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.share_target_login_first),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
|
||||
onFinished()
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
viewModel.initialiseAPI()
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
media?.forEach { uri ->
|
||||
DocumentFile.fromSingleUri(context, uri)?.let { file ->
|
||||
val mFile = File(context.cacheDir, file.name ?: "attachment")
|
||||
|
||||
mFile.outputStream().use { output ->
|
||||
@Suppress("Recycle")
|
||||
context.contentResolver.openInputStream(uri)?.copyTo(output)
|
||||
}
|
||||
|
||||
viewModel.attachments.add(
|
||||
FileArgs(
|
||||
file = mFile,
|
||||
contentType = file.type ?: "application/octet-stream",
|
||||
filename = file.name ?: "attachment",
|
||||
pickerIdentifier = null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
text?.let {
|
||||
viewModel.messageContent = it
|
||||
}
|
||||
}
|
||||
|
||||
var channelSearchContent by remember { mutableStateOf("") }
|
||||
var selectedChannel by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
|
||||
RevoltTheme(
|
||||
requestedTheme = GlobalState.theme,
|
||||
colourOverrides = SyncedSettings.android.colourOverrides
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(title = {
|
||||
Text(text = stringResource(R.string.share))
|
||||
})
|
||||
}
|
||||
) { pv ->
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(pv)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
if (!viewModel.apiIsReady) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
)
|
||||
}
|
||||
return@Surface
|
||||
}
|
||||
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = channelSearchContent,
|
||||
onValueChange = {
|
||||
channelSearchContent = it
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(R.string.share_target_search_channels))
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
val filteredChannels = RevoltAPI.channelCache.values.asSequence().filter {
|
||||
it.name?.contains(
|
||||
channelSearchContent,
|
||||
ignoreCase = true
|
||||
) == true
|
||||
|| ChannelUtils.resolveDMName(it)
|
||||
?.contains(
|
||||
channelSearchContent,
|
||||
ignoreCase = true
|
||||
) == true
|
||||
}
|
||||
|
||||
LazyColumn {
|
||||
items(filteredChannels.count()) {
|
||||
val channel = filteredChannels.elementAt(it)
|
||||
|
||||
DrawerChannel(
|
||||
iconType = DrawerChannelIconType.Channel(
|
||||
channel.channelType ?: ChannelType.TextChannel
|
||||
),
|
||||
name = (if (channel.server != null) "${channel.name} (${RevoltAPI.serverCache[channel.server]?.name})" else channel.name)
|
||||
?: ChannelUtils.resolveDMName(channel)
|
||||
?: stringResource(R.string.unknown),
|
||||
selected = selectedChannel == channel.id,
|
||||
hasUnread = false,
|
||||
onClick = {
|
||||
selectedChannel = channel.id
|
||||
},
|
||||
dmPartnerIcon = ChannelUtils.resolveDMPartner(
|
||||
channel
|
||||
)?.let { u -> RevoltAPI.userCache[u] }?.avatar,
|
||||
dmPartnerName = ChannelUtils.resolveDMName(
|
||||
channel
|
||||
),
|
||||
dmPartnerStatus = ChannelUtils.resolveDMPartner(
|
||||
channel
|
||||
)
|
||||
?.let { u -> RevoltAPI.userCache[u] }?.status?.presence?.let { p ->
|
||||
presenceFromStatus(
|
||||
p,
|
||||
RevoltAPI.userCache[ChannelUtils.resolveDMPartner(
|
||||
channel
|
||||
)]?.online ?: false
|
||||
)
|
||||
},
|
||||
dmPartnerId = ChannelUtils.resolveDMPartner(
|
||||
channel
|
||||
),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
top = 8.dp
|
||||
)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
) {
|
||||
AnimatedVisibility(viewModel.attachments.isNotEmpty()) {
|
||||
AttachmentManager(
|
||||
attachments = viewModel.attachments,
|
||||
uploading = viewModel.attachmentsUploading,
|
||||
uploadProgress = viewModel.attachmentProgress,
|
||||
onRemove = {},
|
||||
canRemove = false
|
||||
)
|
||||
}
|
||||
|
||||
NativeMessageField(
|
||||
value = viewModel.messageContent,
|
||||
onValueChange = { viewModel.messageContent = it },
|
||||
canAttach = false,
|
||||
forceSendButton = viewModel.attachments.isNotEmpty(),
|
||||
disabled = viewModel.attachmentsUploading,
|
||||
onAddAttachment = {},
|
||||
onCommitAttachment = {},
|
||||
onPickEmoji = {
|
||||
if (viewModel.activeBottomPane is BottomPane.EmojiPicker) {
|
||||
viewModel.activeBottomPane = BottomPane.None
|
||||
} else {
|
||||
viewModel.activeBottomPane = BottomPane.EmojiPicker
|
||||
}
|
||||
},
|
||||
onSendMessage = {
|
||||
if (selectedChannel == null) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.share_target_select_channel),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return@NativeMessageField
|
||||
} else {
|
||||
viewModel.send(selectedChannel!!) {
|
||||
onFinished()
|
||||
}
|
||||
}
|
||||
},
|
||||
channelType = RevoltAPI.channelCache[selectedChannel]?.channelType
|
||||
?: ChannelType.TextChannel,
|
||||
channelName = RevoltAPI.channelCache[selectedChannel]?.name ?: "",
|
||||
)
|
||||
|
||||
AnimatedVisibility(viewModel.activeBottomPane is BottomPane.EmojiPicker) {
|
||||
BackHandler(enabled = viewModel.activeBottomPane == BottomPane.EmojiPicker) {
|
||||
viewModel.activeBottomPane = BottomPane.None
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.5f)
|
||||
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp))
|
||||
.padding(4.dp)
|
||||
) {
|
||||
EmojiPicker {
|
||||
viewModel.messageContent += " $it"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ object ChannelUtils {
|
|||
}
|
||||
|
||||
fun resolveDMPartner(channel: Channel): String? {
|
||||
return channel.recipients?.first { u -> u != RevoltAPI.selfId }
|
||||
return channel.recipients?.firstOrNull { u -> u != RevoltAPI.selfId }
|
||||
}
|
||||
|
||||
fun categoriseServerFlat(server: Server): List<CategorisedChannelList> {
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ fun NativeMessageField(
|
|||
channelName: String,
|
||||
modifier: Modifier = Modifier,
|
||||
forceSendButton: Boolean = false,
|
||||
canAttach: Boolean = true,
|
||||
disabled: Boolean = false,
|
||||
serverId: String? = null,
|
||||
channelId: String? = null,
|
||||
|
|
@ -341,29 +342,31 @@ fun NativeMessageField(
|
|||
) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Icon(
|
||||
when {
|
||||
editMode -> Icons.Default.Close
|
||||
else -> Icons.Default.Add
|
||||
},
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||
contentDescription = stringResource(id = R.string.add_attachment_alt),
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.size(32.dp)
|
||||
.clickable {
|
||||
when {
|
||||
editMode -> cancelEdit()
|
||||
else -> {
|
||||
// hide keyboard because it's annoying
|
||||
clearFocus()
|
||||
onAddAttachment()
|
||||
if (canAttach) {
|
||||
Icon(
|
||||
when {
|
||||
editMode -> Icons.Default.Close
|
||||
else -> Icons.Default.Add
|
||||
},
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||
contentDescription = stringResource(id = R.string.add_attachment_alt),
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.size(32.dp)
|
||||
.clickable {
|
||||
when {
|
||||
editMode -> cancelEdit()
|
||||
else -> {
|
||||
// hide keyboard because it's annoying
|
||||
clearFocus()
|
||||
onAddAttachment()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(4.dp)
|
||||
.testTag("add_attachment")
|
||||
)
|
||||
.padding(4.dp)
|
||||
.testTag("add_attachment")
|
||||
)
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
|
|
@ -599,6 +602,7 @@ fun NativeMessageFieldPreview() {
|
|||
channelName = "Test",
|
||||
modifier = Modifier,
|
||||
forceSendButton = false,
|
||||
canAttach = true,
|
||||
disabled = false,
|
||||
editMode = false,
|
||||
cancelEdit = {},
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ fun AttachmentManager(
|
|||
attachments: List<FileArgs>,
|
||||
uploading: Boolean,
|
||||
uploadProgress: Float = 0f,
|
||||
onRemove: (FileArgs) -> Unit
|
||||
onRemove: (FileArgs) -> Unit,
|
||||
canRemove: Boolean = true
|
||||
) {
|
||||
val animatedProgress by animateFloatAsState(
|
||||
targetValue = uploadProgress,
|
||||
|
|
@ -69,11 +70,13 @@ fun AttachmentManager(
|
|||
.padding(8.dp)
|
||||
) {
|
||||
Text(attachment.filename, maxLines = 1)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = stringResource(R.string.remove_attachment_alt)
|
||||
)
|
||||
if (canRemove) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = stringResource(R.string.remove_attachment_alt)
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -529,4 +529,11 @@
|
|||
<string name="settings_changelogs_new_header">What\'s been cooking ✨</string>
|
||||
<string name="settings_changelogs_historical_version_header">Changelog for %1$s</string>
|
||||
<string name="settings_changelogs_historical_version_header_placeholder">that version</string>
|
||||
|
||||
<string name="share_target_heading">Share to Revolt</string>
|
||||
<string name="share_target_login_first">Please log in before sharing to Revolt.</string>
|
||||
<string name="share_target_invalid_intent">This is not a valid share intent.</string>
|
||||
<string name="share_target_attachment_too_large">This attachment is too large for Revolt (max. $1$s).</string>
|
||||
<string name="share_target_search_channels">Search channels</string>
|
||||
<string name="share_target_select_channel">Please select a channel to share to.</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Reference in New Issue