feat: pinned messages view and system message
This commit is contained in:
parent
2427f9e6d4
commit
058dcbcd50
|
|
@ -82,19 +82,20 @@ import chat.stoat.api.api
|
||||||
import chat.stoat.api.routes.microservices.geo.queryGeo
|
import chat.stoat.api.routes.microservices.geo.queryGeo
|
||||||
import chat.stoat.api.routes.microservices.health.healthCheck
|
import chat.stoat.api.routes.microservices.health.healthCheck
|
||||||
import chat.stoat.api.routes.onboard.needsOnboarding
|
import chat.stoat.api.routes.onboard.needsOnboarding
|
||||||
import chat.stoat.core.model.schemas.HealthNotice
|
|
||||||
import chat.stoat.api.settings.Experiments
|
import chat.stoat.api.settings.Experiments
|
||||||
import chat.stoat.api.settings.GeoStateProvider
|
import chat.stoat.api.settings.GeoStateProvider
|
||||||
import chat.stoat.api.settings.LoadedSettings
|
import chat.stoat.api.settings.LoadedSettings
|
||||||
import chat.stoat.api.settings.SyncedSettings
|
import chat.stoat.api.settings.SyncedSettings
|
||||||
import chat.stoat.composables.generic.HealthAlert
|
import chat.stoat.composables.generic.HealthAlert
|
||||||
import chat.stoat.composables.voice.VoicePermissionSwitch
|
import chat.stoat.composables.voice.VoicePermissionSwitch
|
||||||
|
import chat.stoat.core.model.schemas.HealthNotice
|
||||||
import chat.stoat.material.EasingTokens
|
import chat.stoat.material.EasingTokens
|
||||||
import chat.stoat.ndk.NativeLibraries
|
import chat.stoat.ndk.NativeLibraries
|
||||||
import chat.stoat.persistence.KVStorage
|
import chat.stoat.persistence.KVStorage
|
||||||
import chat.stoat.screens.DefaultDestinationScreen
|
import chat.stoat.screens.DefaultDestinationScreen
|
||||||
import chat.stoat.screens.about.AboutScreen
|
import chat.stoat.screens.about.AboutScreen
|
||||||
import chat.stoat.screens.about.AttributionScreen
|
import chat.stoat.screens.about.AttributionScreen
|
||||||
|
import chat.stoat.screens.chat.ChannelPinsScreen
|
||||||
import chat.stoat.screens.chat.ChatRouterScreen
|
import chat.stoat.screens.chat.ChatRouterScreen
|
||||||
import chat.stoat.screens.chat.standalone.CatchUpScreen
|
import chat.stoat.screens.chat.standalone.CatchUpScreen
|
||||||
import chat.stoat.screens.chat.views.channel.ChannelScreen
|
import chat.stoat.screens.chat.views.channel.ChannelScreen
|
||||||
|
|
@ -733,6 +734,11 @@ fun AppEntrypoint(
|
||||||
ChannelSettingsPermissions(navController, channelId)
|
ChannelSettingsPermissions(navController, channelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable("channel/{channelId}/pins") { backStackEntry ->
|
||||||
|
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
||||||
|
ChannelPinsScreen(navController, channelId)
|
||||||
|
}
|
||||||
|
|
||||||
composable("about") { AboutScreen(navController) }
|
composable("about") { AboutScreen(navController) }
|
||||||
composable("about/oss") { AttributionScreen(navController) }
|
composable("about/oss") { AttributionScreen(navController) }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -255,4 +255,84 @@ suspend fun patchChannel(
|
||||||
val channel = StoatJson.decodeFromString(Channel.serializer(), response)
|
val channel = StoatJson.decodeFromString(Channel.serializer(), response)
|
||||||
StoatAPI.channelCache[channelId] = channel
|
StoatAPI.channelCache[channelId] = channel
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun searchChannel(
|
||||||
|
channelId: String,
|
||||||
|
query: String? = null,
|
||||||
|
pinned: Boolean? = null,
|
||||||
|
includeUsers: Boolean? = null,
|
||||||
|
after: String? = null,
|
||||||
|
before: String? = null,
|
||||||
|
limit: Int? = null,
|
||||||
|
sort: String? = null
|
||||||
|
): MessagesInChannel {
|
||||||
|
check(
|
||||||
|
sort == null || sort in listOf(
|
||||||
|
"Relevance",
|
||||||
|
"Latest",
|
||||||
|
"Oldest"
|
||||||
|
)
|
||||||
|
) { "Sort must be one of Relevance, Latest, Oldest; failing that null" }
|
||||||
|
check(limit == null || (limit in 1..100)) { "Limit must be between 1 and 100; failing that null" }
|
||||||
|
check(query == null || pinned == null) { "One of query or pinned must be null" }
|
||||||
|
check(
|
||||||
|
pinned != null || (!query.isNullOrBlank() && query.length <= 64)
|
||||||
|
) { "Query must not be null when pinned is not null; Query must not be blank when pinned is not null; Query must be less than 65 characters when pinned is not null" }
|
||||||
|
|
||||||
|
val body = mutableMapOf<String, JsonElement>()
|
||||||
|
|
||||||
|
if (query != null) {
|
||||||
|
body["query"] = StoatJson.encodeToJsonElement(String.serializer(), query)
|
||||||
|
}
|
||||||
|
if (pinned != null) {
|
||||||
|
body["pinned"] = StoatJson.encodeToJsonElement(Boolean.serializer(), pinned)
|
||||||
|
}
|
||||||
|
if (includeUsers != null) {
|
||||||
|
body["include_users"] = StoatJson.encodeToJsonElement(Boolean.serializer(), includeUsers)
|
||||||
|
}
|
||||||
|
if (after != null) {
|
||||||
|
body["after"] = StoatJson.encodeToJsonElement(String.serializer(), after)
|
||||||
|
}
|
||||||
|
if (before != null) {
|
||||||
|
body["before"] = StoatJson.encodeToJsonElement(String.serializer(), before)
|
||||||
|
}
|
||||||
|
if (limit != null) {
|
||||||
|
body["limit"] = StoatJson.encodeToJsonElement(Int.serializer(), limit)
|
||||||
|
}
|
||||||
|
if (sort != null) {
|
||||||
|
body["sort"] = StoatJson.encodeToJsonElement(String.serializer(), sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = StoatHttp.post("/channels/$channelId/search".api()) {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(
|
||||||
|
StoatJson.encodeToString(
|
||||||
|
MapSerializer(
|
||||||
|
String.serializer(),
|
||||||
|
JsonElement.serializer()
|
||||||
|
),
|
||||||
|
body
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.bodyAsText()
|
||||||
|
|
||||||
|
if (includeUsers == true) {
|
||||||
|
return StoatJson.decodeFromString(
|
||||||
|
MessagesInChannel.serializer(),
|
||||||
|
response
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val messages = StoatJson.decodeFromString(
|
||||||
|
ListSerializer(Message.serializer()),
|
||||||
|
response
|
||||||
|
)
|
||||||
|
|
||||||
|
return MessagesInChannel(
|
||||||
|
messages = messages,
|
||||||
|
users = emptyList(),
|
||||||
|
members = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -42,6 +42,8 @@ enum class SystemMessageType(val type: String) {
|
||||||
USER_KICKED("user_kicked"),
|
USER_KICKED("user_kicked"),
|
||||||
USER_LEFT("user_left"),
|
USER_LEFT("user_left"),
|
||||||
USER_JOINED("user_joined"),
|
USER_JOINED("user_joined"),
|
||||||
|
MESSAGE_PINNED("message_pinned"),
|
||||||
|
MESSAGE_UNPINNED("message_unpinned"),
|
||||||
TEXT("text")
|
TEXT("text")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,6 +174,24 @@ fun SystemMessage(message: Message) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SystemMessageType.MESSAGE_PINNED -> {
|
||||||
|
RichMarkdown(
|
||||||
|
stringResource(
|
||||||
|
R.string.system_message_message_pinned,
|
||||||
|
message.system!!.by.mention()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemMessageType.MESSAGE_UNPINNED -> {
|
||||||
|
RichMarkdown(
|
||||||
|
stringResource(
|
||||||
|
R.string.system_message_message_unpinned,
|
||||||
|
message.system!!.by.mention()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
SystemMessageType.TEXT -> {
|
SystemMessageType.TEXT -> {
|
||||||
message.system!!.content?.let { RichMarkdown(it) }
|
message.system!!.content?.let { RichMarkdown(it) }
|
||||||
}
|
}
|
||||||
|
|
@ -277,6 +297,24 @@ fun SystemMessageIcon(type: SystemMessageType, modifier: Modifier = Modifier, si
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SystemMessageType.MESSAGE_PINNED -> {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_keep_24dp),
|
||||||
|
contentDescription = stringResource(R.string.system_message_message_pinned_alt),
|
||||||
|
tint = LocalContentColor.current,
|
||||||
|
modifier = modifier.size(size)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemMessageType.MESSAGE_UNPINNED -> {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_keep_off_24dp),
|
||||||
|
contentDescription = stringResource(R.string.system_message_message_unpinned_alt),
|
||||||
|
tint = LocalContentColor.current,
|
||||||
|
modifier = modifier.size(size)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
SystemMessageType.TEXT -> {
|
SystemMessageType.TEXT -> {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_info_24dp),
|
painter = painterResource(R.drawable.ic_info_24dp),
|
||||||
|
|
@ -302,6 +340,8 @@ private fun shapeForType(type: SystemMessageType): Shape {
|
||||||
SystemMessageType.USER_KICKED -> MaterialShapes.SoftBurst
|
SystemMessageType.USER_KICKED -> MaterialShapes.SoftBurst
|
||||||
SystemMessageType.USER_LEFT -> MaterialShapes.Cookie4Sided
|
SystemMessageType.USER_LEFT -> MaterialShapes.Cookie4Sided
|
||||||
SystemMessageType.USER_JOINED -> MaterialShapes.Cookie9Sided
|
SystemMessageType.USER_JOINED -> MaterialShapes.Cookie9Sided
|
||||||
|
SystemMessageType.MESSAGE_PINNED -> MaterialShapes.Clover4Leaf
|
||||||
|
SystemMessageType.MESSAGE_UNPINNED -> MaterialShapes.Clover8Leaf
|
||||||
SystemMessageType.TEXT -> MaterialShapes.Square
|
SystemMessageType.TEXT -> MaterialShapes.Square
|
||||||
}.toShape()
|
}.toShape()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
package chat.stoat.screens.chat
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.statusBars
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import chat.stoat.R
|
||||||
|
import chat.stoat.api.StoatAPI
|
||||||
|
import chat.stoat.api.routes.channel.searchChannel
|
||||||
|
import chat.stoat.composables.chat.SystemMessage
|
||||||
|
import chat.stoat.core.model.schemas.Message
|
||||||
|
import chat.stoat.internals.extensions.zero
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class ChannelPinsScreenViewModel : ViewModel() {
|
||||||
|
val pinnedMessages = mutableStateListOf<Message>()
|
||||||
|
var isLoading by mutableStateOf(false)
|
||||||
|
var error by mutableStateOf<String?>(null)
|
||||||
|
|
||||||
|
fun loadPins(channelId: String) {
|
||||||
|
pinnedMessages.clear()
|
||||||
|
isLoading = true
|
||||||
|
error = null
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val response =
|
||||||
|
searchChannel(channelId, pinned = true, limit = 100, includeUsers = true)
|
||||||
|
|
||||||
|
response.users?.forEach { user ->
|
||||||
|
user.id?.let { id ->
|
||||||
|
StoatAPI.userCache.putIfAbsent(id, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.members?.forEach { member ->
|
||||||
|
member.id?.let { memberId ->
|
||||||
|
if (!StoatAPI.members.hasMember(memberId.server, memberId.user)) {
|
||||||
|
StoatAPI.members.setMember(memberId.server, member)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pinnedMessages.addAll(response.messages ?: emptyList())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ChannelPinsScreen", "Failed to load pinned messages", e)
|
||||||
|
error = e.message
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ChannelPinsScreen(
|
||||||
|
navController: NavController,
|
||||||
|
channelId: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: ChannelPinsScreenViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
LaunchedEffect(channelId) {
|
||||||
|
viewModel.loadPins(channelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
Column {
|
||||||
|
AnimatedVisibility(LocalIsConnected.current) {
|
||||||
|
Spacer(
|
||||||
|
Modifier
|
||||||
|
.height(
|
||||||
|
WindowInsets.statusBars.asPaddingValues()
|
||||||
|
.calculateTopPadding()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.pinned_messages),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
navController.popBackStack()
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_arrow_back_24dp),
|
||||||
|
contentDescription = stringResource(id = R.string.back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentWindowInsets = WindowInsets.zero
|
||||||
|
) { pv ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(pv)
|
||||||
|
.fillMaxHeight()
|
||||||
|
) {
|
||||||
|
Crossfade(targetState = viewModel.isLoading, label = "pinsLoading") { loading ->
|
||||||
|
if (loading) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(48.dp))
|
||||||
|
}
|
||||||
|
} else if (viewModel.pinnedMessages.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text(
|
||||||
|
text = viewModel.error
|
||||||
|
?: stringResource(R.string.pinned_messages_empty),
|
||||||
|
color = if (viewModel.error != null) MaterialTheme.colorScheme.error else LocalContentColor.current,
|
||||||
|
modifier = Modifier.padding(horizontal = 64.dp),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
if (viewModel.error != null) {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
TextButton(onClick = { viewModel.loadPins(channelId) }) {
|
||||||
|
Text(stringResource(R.string.tap_to_retry))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(contentPadding = WindowInsets.navigationBars.asPaddingValues()) {
|
||||||
|
items(
|
||||||
|
viewModel.pinnedMessages.size,
|
||||||
|
key = { i -> viewModel.pinnedMessages[i].id ?: i }) { i ->
|
||||||
|
val message = viewModel.pinnedMessages[i].copy(tail = false)
|
||||||
|
if (message.system != null) {
|
||||||
|
SystemMessage(message)
|
||||||
|
} else {
|
||||||
|
chat.stoat.composables.chat.Message(message = message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -639,6 +639,18 @@ fun ChannelScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
scope.launch {
|
||||||
|
ActionChannel.send(Action.TopNavigate("channel/$channelId/pins"))
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_pinboard_24dp),
|
||||||
|
contentDescription = stringResource(id = R.string.pinned_messages_view)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -669,7 +681,7 @@ fun ChannelScreen(
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator(modifier = Modifier.size(48.dp))
|
CircularProgressIndicator(modifier = Modifier.size(48.dp))
|
||||||
}
|
}
|
||||||
} else if (ageGateUnlocked == true) {
|
} else if (ageGateUnlocked) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(pv)
|
.padding(pv)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M680,120L680,200L640,200L640,527L560,447L560,200L400,200L400,287L313,200L280,167L280,167L280,120L680,120ZM480,920L440,880L440,640L240,640L240,560L320,480L320,434L56,168L112,112L848,848L790,904L526,640L520,640L520,880L480,920ZM354,560L446,560L402,516L400,514L354,560ZM480,367L480,367L480,367L480,367ZM402,516L402,516L402,516L402,516Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M260,880L220,840L220,680L80,680L80,600L140,494L140,400L80,400L80,320L440,320L440,400L380,400L380,494L440,600L440,680L300,680L300,840L260,880ZM480,800L480,720L800,720Q800,720 800,720Q800,720 800,720L800,240Q800,240 800,240Q800,240 800,240L80,240L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800L480,800ZM172,600L348,600L300,516L300,400L220,400L220,516L172,600ZM260,600L260,600L260,600L260,600L260,600L260,600Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -277,6 +277,8 @@
|
||||||
<string name="system_message_user_kicked_alt">User kicked</string>
|
<string name="system_message_user_kicked_alt">User kicked</string>
|
||||||
<string name="system_message_user_left_alt">User left</string>
|
<string name="system_message_user_left_alt">User left</string>
|
||||||
<string name="system_message_user_joined_alt">User joined</string>
|
<string name="system_message_user_joined_alt">User joined</string>
|
||||||
|
<string name="system_message_message_pinned_alt">Message pinned</string>
|
||||||
|
<string name="system_message_message_unpinned_alt">Message unpinned</string>
|
||||||
<string name="system_message_text_alt">System message</string>
|
<string name="system_message_text_alt">System message</string>
|
||||||
|
|
||||||
<string name="system_message_ownership_changed">%1$s transferred ownership to %2$s</string>
|
<string name="system_message_ownership_changed">%1$s transferred ownership to %2$s</string>
|
||||||
|
|
@ -289,6 +291,8 @@
|
||||||
<string name="system_message_user_kicked">%1$s has been kicked</string>
|
<string name="system_message_user_kicked">%1$s has been kicked</string>
|
||||||
<string name="system_message_user_left">%1$s left</string>
|
<string name="system_message_user_left">%1$s left</string>
|
||||||
<string name="system_message_user_joined">%1$s joined</string>
|
<string name="system_message_user_joined">%1$s joined</string>
|
||||||
|
<string name="system_message_message_pinned">%1$s pinned a message to this channel</string>
|
||||||
|
<string name="system_message_message_unpinned">%1$s unpinned a message from this channel</string>
|
||||||
|
|
||||||
<string name="today">Today</string>
|
<string name="today">Today</string>
|
||||||
<string name="yesterday">Yesterday</string>
|
<string name="yesterday">Yesterday</string>
|
||||||
|
|
@ -377,6 +381,10 @@
|
||||||
<string name="invite_dialog_copy">Copy</string>
|
<string name="invite_dialog_copy">Copy</string>
|
||||||
<string name="invite_dialog_close">Close</string>
|
<string name="invite_dialog_close">Close</string>
|
||||||
|
|
||||||
|
<string name="pinned_messages">Pinned Messages</string>
|
||||||
|
<string name="pinned_messages_view">View Pinned Messages</string>
|
||||||
|
<string name="pinned_messages_empty">No pinned messages yet, maybe you should pin one?</string>
|
||||||
|
|
||||||
<string name="message_context_sheet_actions_copy">Copy</string>
|
<string name="message_context_sheet_actions_copy">Copy</string>
|
||||||
<string name="message_context_sheet_actions_copy_failed_empty">Message is empty, nothing to copy</string>
|
<string name="message_context_sheet_actions_copy_failed_empty">Message is empty, nothing to copy</string>
|
||||||
<string name="message_context_sheet_actions_reply">Reply</string>
|
<string name="message_context_sheet_actions_reply">Reply</string>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue