feat: user action buttons
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
adba9da754
commit
9159f33c3b
|
|
@ -28,6 +28,7 @@ import chat.revolt.api.realtime.frames.receivable.UserUpdateFrame
|
|||
import chat.revolt.api.realtime.frames.sendable.AuthorizationFrame
|
||||
import chat.revolt.api.realtime.frames.sendable.PingFrame
|
||||
import chat.revolt.api.routes.server.fetchMember
|
||||
import chat.revolt.api.schemas.Channel
|
||||
import chat.revolt.api.settings.GlobalState
|
||||
import chat.revolt.api.settings.SyncedSettings
|
||||
import io.ktor.client.plugins.websocket.ws
|
||||
|
|
@ -304,6 +305,18 @@ object RealtimeSocket {
|
|||
existing.mergeWithPartial(channelUpdateFrame.data)
|
||||
}
|
||||
|
||||
"ChannelCreate" -> {
|
||||
val channelCreateFrame =
|
||||
RevoltJson.decodeFromString(Channel.serializer(), rawFrame)
|
||||
|
||||
Log.d(
|
||||
"RealtimeSocket",
|
||||
"Received channel create frame for ${channelCreateFrame.id}, with name ${channelCreateFrame.name}. Adding to cache."
|
||||
)
|
||||
|
||||
RevoltAPI.channelCache[channelCreateFrame.id!!] = channelCreateFrame
|
||||
}
|
||||
|
||||
"ChannelAck" -> {
|
||||
val channelAckFrame =
|
||||
RevoltJson.decodeFromString(ChannelAckFrame.serializer(), rawFrame)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package chat.revolt.api.routes.user
|
||||
|
||||
import chat.revolt.api.RevoltError
|
||||
import chat.revolt.api.RevoltHttp
|
||||
import chat.revolt.api.RevoltJson
|
||||
import chat.revolt.api.schemas.Channel
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import kotlinx.serialization.SerializationException
|
||||
|
||||
suspend fun openDM(userId: String): Channel {
|
||||
val response = RevoltHttp.get("/users/$userId/dm")
|
||||
.bodyAsText()
|
||||
|
||||
try {
|
||||
val error = RevoltJson.decodeFromString(RevoltError.serializer(), response)
|
||||
throw Error(error.type)
|
||||
} catch (e: SerializationException) {
|
||||
// Not an error
|
||||
}
|
||||
|
||||
return RevoltJson.decodeFromString(Channel.serializer(), response)
|
||||
}
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
package chat.revolt.api.routes.user
|
||||
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.RevoltError
|
||||
import chat.revolt.api.RevoltHttp
|
||||
import chat.revolt.api.RevoltJson
|
||||
import io.ktor.client.request.delete
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.put
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.contentType
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlin.collections.set
|
||||
|
||||
suspend fun blockUser(userId: String) {
|
||||
val response = RevoltHttp.put("/users/$userId/block")
|
||||
|
|
@ -20,9 +22,6 @@ suspend fun blockUser(userId: String) {
|
|||
} catch (e: SerializationException) {
|
||||
// Not an error
|
||||
}
|
||||
|
||||
val user = RevoltAPI.userCache[userId] ?: return
|
||||
RevoltAPI.userCache[userId] = user.copy(relationship = "Blocked")
|
||||
}
|
||||
|
||||
suspend fun unblockUser(userId: String) {
|
||||
|
|
@ -35,9 +34,33 @@ suspend fun unblockUser(userId: String) {
|
|||
} catch (e: SerializationException) {
|
||||
// Not an error
|
||||
}
|
||||
}
|
||||
|
||||
val user = RevoltAPI.userCache[userId] ?: return
|
||||
RevoltAPI.userCache[userId] = user.copy(relationship = "None")
|
||||
suspend fun friendUser(username: String) {
|
||||
val response = RevoltHttp.post("/users/friend") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(mapOf("username" to username))
|
||||
}
|
||||
val body = response.bodyAsText()
|
||||
|
||||
try {
|
||||
val error = RevoltJson.decodeFromString(RevoltError.serializer(), body)
|
||||
throw Error(error.type)
|
||||
} catch (e: SerializationException) {
|
||||
// Not an error
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun acceptFriendRequest(userId: String) {
|
||||
val response = RevoltHttp.put("/users/$userId/friend")
|
||||
.bodyAsText()
|
||||
|
||||
try {
|
||||
val error = RevoltJson.decodeFromString(RevoltError.serializer(), response)
|
||||
throw Error(error.type)
|
||||
} catch (e: SerializationException) {
|
||||
// Not an error
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun unfriendUser(userId: String) {
|
||||
|
|
@ -50,7 +73,4 @@ suspend fun unfriendUser(userId: String) {
|
|||
} catch (e: SerializationException) {
|
||||
// Not an error
|
||||
}
|
||||
|
||||
val user = RevoltAPI.userCache[userId] ?: return
|
||||
RevoltAPI.userCache[userId] = user.copy(relationship = "None")
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ sealed class Action {
|
|||
data class SwitchChannel(val channelId: String) : Action()
|
||||
data class LinkInfo(val url: String) : Action()
|
||||
data class EmoteInfo(val emoteId: String) : Action()
|
||||
data class TopNavigate(val route: String) : Action()
|
||||
data class ChatNavigate(val route: String) : Action()
|
||||
}
|
||||
|
||||
val ActionChannel = Channel<Action>(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,256 @@
|
|||
package chat.revolt.components.screens.settings
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.revolt.R
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.routes.user.acceptFriendRequest
|
||||
import chat.revolt.api.routes.user.blockUser
|
||||
import chat.revolt.api.routes.user.friendUser
|
||||
import chat.revolt.api.routes.user.openDM
|
||||
import chat.revolt.api.routes.user.unblockUser
|
||||
import chat.revolt.api.routes.user.unfriendUser
|
||||
import chat.revolt.api.schemas.User
|
||||
import chat.revolt.callbacks.Action
|
||||
import chat.revolt.callbacks.ActionChannel
|
||||
import chat.revolt.internals.Platform
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun UserButtons(
|
||||
user: User,
|
||||
dismissSheet: suspend () -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val clipboard = LocalClipboardManager.current
|
||||
|
||||
var menuOpen by remember { mutableStateOf(false) }
|
||||
|
||||
if (user.id == null) return Row {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
friendUser("${user.username}#${user.discriminator}")
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(stringResource(R.string.user_info_sheet_add_friend))
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
when (user.relationship) {
|
||||
"None" -> {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
friendUser("${user.username}#${user.discriminator}")
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(stringResource(R.string.user_info_sheet_add_friend))
|
||||
}
|
||||
}
|
||||
|
||||
"User" -> {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
ActionChannel.send(Action.TopNavigate("settings/profile"))
|
||||
// We must now close the bottom sheet,
|
||||
// else we will crash if we try to open this sheet again
|
||||
dismissSheet()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(stringResource(R.string.user_info_sheet_edit_profile))
|
||||
}
|
||||
}
|
||||
|
||||
"Friend" -> {
|
||||
TextButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val dm = openDM(user.id)
|
||||
if (dm.id != null) {
|
||||
if (RevoltAPI.channelCache[dm.id] == null)
|
||||
RevoltAPI.channelCache[dm.id] = dm
|
||||
ActionChannel.send(Action.SwitchChannel(dm.id))
|
||||
dismissSheet()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.user_info_sheet_failed_to_open_dm),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(stringResource(R.string.user_info_sheet_send_message))
|
||||
}
|
||||
// Remove friend (in overflow menu)
|
||||
}
|
||||
|
||||
"Outgoing" -> {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
unfriendUser(user.id)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(stringResource(R.string.user_info_sheet_cancel_request))
|
||||
}
|
||||
}
|
||||
|
||||
"Incoming" -> {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
acceptFriendRequest(user.id)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(stringResource(R.string.user_info_sheet_accept_request))
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
unfriendUser(user.id)
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error,
|
||||
contentColor = MaterialTheme.colorScheme.onError,
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(stringResource(R.string.user_info_sheet_decline_request))
|
||||
}
|
||||
}
|
||||
|
||||
"Blocked" -> {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
unblockUser(user.id)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(stringResource(R.string.user_info_sheet_unblock))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (user.relationship) {
|
||||
"Friend", "Incoming", "Outgoing", "None" -> {
|
||||
Column { // Prevent the dropdown menu from counting towards arrangement spacing
|
||||
IconButton(
|
||||
onClick = {
|
||||
menuOpen = true
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = stringResource(R.string.menu)
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) {
|
||||
when (user.relationship) {
|
||||
"Friend" -> {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringResource(R.string.user_info_sheet_remove_friend))
|
||||
},
|
||||
onClick = {
|
||||
scope.launch {
|
||||
unfriendUser(user.id)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringResource(R.string.user_info_sheet_block))
|
||||
},
|
||||
onClick = {
|
||||
scope.launch {
|
||||
blockUser(user.id)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringResource(R.string.user_info_sheet_copy_id))
|
||||
},
|
||||
onClick = {
|
||||
scope.launch {
|
||||
clipboard.setText(AnnotatedString(user.id))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringResource(R.string.user_info_sheet_report))
|
||||
},
|
||||
onClick = {
|
||||
scope.launch {
|
||||
ActionChannel.send(Action.ChatNavigate("report/user/${user.id}"))
|
||||
|
||||
if (Platform.needsShowClipboardNotification()) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.copied),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import androidx.compose.foundation.verticalScroll
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DismissibleDrawerSheet
|
||||
import androidx.compose.material3.DismissibleNavigationDrawer
|
||||
import androidx.compose.material3.DrawerState
|
||||
|
|
@ -439,6 +440,14 @@ fun ChatRouterScreen(
|
|||
emoteInfoSheetTarget = action.emoteId
|
||||
showEmoteInfoSheet = true
|
||||
}
|
||||
|
||||
is Action.TopNavigate -> {
|
||||
topNav.navigate(action.route)
|
||||
}
|
||||
|
||||
is Action.ChatNavigate -> {
|
||||
navController.navigate(action.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -586,7 +595,11 @@ fun ChatRouterScreen(
|
|||
) {
|
||||
UserInfoSheet(
|
||||
userId = userContextSheetTarget,
|
||||
serverId = userContextSheetServer
|
||||
serverId = userContextSheetServer,
|
||||
dismissSheet = {
|
||||
userContextSheetState.hide()
|
||||
showUserContextSheet = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -954,6 +967,7 @@ fun Sidebar(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChannelNavigator(
|
||||
navController: NavHostController,
|
||||
|
|
@ -1032,6 +1046,22 @@ fun ChannelNavigator(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
dialog("report/user/{userId}") { backStackEntry ->
|
||||
val userId = backStackEntry.arguments?.getString("userId")
|
||||
if (userId != null) {
|
||||
AlertDialog(onDismissRequest = {
|
||||
navController.popBackStack()
|
||||
}) {
|
||||
Text("Report user $userId")
|
||||
Button(onClick = {
|
||||
navController.popBackStack()
|
||||
}) {
|
||||
Text("Close")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ class ProfileSettingsScreenViewModel @Inject constructor(@ApplicationContext val
|
|||
fun saveBio() {
|
||||
viewModelScope.launch {
|
||||
patchSelf(bio = pendingProfile?.content)
|
||||
|
||||
|
||||
fetchUserProfile(RevoltAPI.selfId!!).let {
|
||||
currentProfile = it
|
||||
pendingProfile = it
|
||||
|
|
@ -332,7 +332,7 @@ fun ProfileSettingsScreen(
|
|||
},
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.user_context_sheet_category_bio),
|
||||
text = stringResource(id = R.string.user_info_sheet_category_bio),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -247,7 +247,11 @@ fun MemberListSheet(
|
|||
) {
|
||||
UserInfoSheet(
|
||||
userId = userContextSheetTarget,
|
||||
serverId = serverId
|
||||
serverId = serverId,
|
||||
dismissSheet = {
|
||||
userContextSheetState.hide()
|
||||
showUserContextSheet = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,10 +37,15 @@ import chat.revolt.components.chat.RoleChip
|
|||
import chat.revolt.components.generic.NonIdealState
|
||||
import chat.revolt.components.generic.WebMarkdown
|
||||
import chat.revolt.components.screens.settings.RawUserOverview
|
||||
import chat.revolt.components.screens.settings.UserButtons
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun UserInfoSheet(userId: String, serverId: String? = null) {
|
||||
fun UserInfoSheet(
|
||||
userId: String,
|
||||
serverId: String? = null,
|
||||
dismissSheet: suspend () -> Unit
|
||||
) {
|
||||
val user = RevoltAPI.userCache[userId]
|
||||
|
||||
val member = serverId?.let { RevoltAPI.members.getMember(it, userId) }
|
||||
|
|
@ -73,12 +78,12 @@ fun UserInfoSheet(userId: String, serverId: String? = null) {
|
|||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.user_context_sheet_user_not_found)
|
||||
text = stringResource(R.string.user_info_sheet_user_not_found)
|
||||
)
|
||||
},
|
||||
description = {
|
||||
Text(
|
||||
text = stringResource(R.string.user_context_sheet_user_not_found_description)
|
||||
text = stringResource(R.string.user_info_sheet_user_not_found_description)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
@ -94,9 +99,14 @@ fun UserInfoSheet(userId: String, serverId: String? = null) {
|
|||
Column(
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp, top = 8.dp)
|
||||
) {
|
||||
UserButtons(
|
||||
user,
|
||||
dismissSheet
|
||||
)
|
||||
|
||||
member?.roles?.let {
|
||||
Text(
|
||||
text = stringResource(id = R.string.user_context_sheet_category_roles),
|
||||
text = stringResource(id = R.string.user_info_sheet_category_roles),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(vertical = 10.dp)
|
||||
)
|
||||
|
|
@ -121,7 +131,7 @@ fun UserInfoSheet(userId: String, serverId: String? = null) {
|
|||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.user_context_sheet_category_bio),
|
||||
text = stringResource(id = R.string.user_info_sheet_category_bio),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(vertical = 10.dp)
|
||||
)
|
||||
|
|
@ -133,12 +143,12 @@ fun UserInfoSheet(userId: String, serverId: String? = null) {
|
|||
)
|
||||
} else if (profile != null) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.user_context_sheet_bio_empty),
|
||||
text = stringResource(id = R.string.user_info_sheet_bio_empty),
|
||||
color = LocalContentColor.current.copy(alpha = 0.6f)
|
||||
)
|
||||
} else if (profileNotFound) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.user_context_sheet_bio_not_found),
|
||||
text = stringResource(id = R.string.user_info_sheet_bio_not_found),
|
||||
color = LocalContentColor.current.copy(alpha = 0.6f)
|
||||
)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -270,12 +270,25 @@
|
|||
<string name="server_context_sheet_actions_leave_confirm_no">Stay</string>
|
||||
<string name="server_context_sheet_actions_leave_silently">Leave Silently</string>
|
||||
|
||||
<string name="user_context_sheet_user_not_found">Can\'t resolve this user</string>
|
||||
<string name="user_context_sheet_user_not_found_description">This user may have been deleted or you may not have permission to view them.</string>
|
||||
<string name="user_context_sheet_category_bio">Bio</string>
|
||||
<string name="user_context_sheet_bio_empty">This user hasn\'t set a bio yet.</string>
|
||||
<string name="user_context_sheet_bio_not_found">This user\'s bio could not be fetched. Please verify you share a server or are friends.</string>
|
||||
<string name="user_context_sheet_category_roles">Roles</string>
|
||||
<string name="user_info_sheet_user_not_found">Can\'t resolve this user</string>
|
||||
<string name="user_info_sheet_user_not_found_description">This user may have been deleted or you may not have permission to view them.</string>
|
||||
<string name="user_info_sheet_category_bio">Bio</string>
|
||||
<string name="user_info_sheet_bio_empty">This user hasn\'t set a bio yet.</string>
|
||||
<string name="user_info_sheet_bio_not_found">This user\'s bio could not be fetched. Please verify you share a server or are friends.</string>
|
||||
<string name="user_info_sheet_category_roles">Roles</string>
|
||||
<string name="user_info_sheet_add_friend">Add Friend</string>
|
||||
<string name="user_info_sheet_send_message">Send Message</string>
|
||||
<string name="user_info_sheet_remove_friend">Remove Friend</string>
|
||||
<string name="user_info_sheet_accept_request">Accept</string>
|
||||
<string name="user_info_sheet_decline_request">Decline</string>
|
||||
<string name="user_info_sheet_cancel_request">Cancel Request</string>
|
||||
<string name="user_info_sheet_block">Block</string>
|
||||
<string name="user_info_sheet_unblock">Unblock</string>
|
||||
<string name="user_info_sheet_edit_profile">Edit Profile</string>
|
||||
<string name="user_info_sheet_report">Report</string>
|
||||
<string name="user_info_sheet_copy_id">Copy ID</string>
|
||||
<string name="user_info_sheet_failed_to_open_dm">Could not open DM with this user.</string>
|
||||
|
||||
|
||||
<string name="add_server_sheet_title">Add a server</string>
|
||||
<string name="add_server_sheet_join_by_invite">Join by invite code or link</string>
|
||||
|
|
|
|||
Loading…
Reference in New Issue