feat: user action buttons

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2023-11-11 16:25:24 +01:00
parent adba9da754
commit 9159f33c3b
10 changed files with 398 additions and 27 deletions

View File

@ -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)

View File

@ -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)
}

View File

@ -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")
}

View File

@ -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>(

View File

@ -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()
}
}
}
)
}
}
}
}
}
}

View File

@ -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")
}
}
}
}
}
}
}

View File

@ -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,
)
},

View File

@ -247,7 +247,11 @@ fun MemberListSheet(
) {
UserInfoSheet(
userId = userContextSheetTarget,
serverId = serverId
serverId = serverId,
dismissSheet = {
userContextSheetState.hide()
showUserContextSheet = false
}
)
}
}

View File

@ -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 {

View File

@ -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>