feat: remove members from group DMs

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2024-03-16 15:11:28 +01:00
parent 8b276aade3
commit a5b240f5fa
6 changed files with 248 additions and 25 deletions

2
.idea/.gitignore vendored
View File

@ -6,3 +6,5 @@
/other.xml
# GitHub Copilot persisted chat sessions
/copilot/chatSessions
# User-specific files
/deploymentTargetSelector.xml

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

View File

@ -5,11 +5,14 @@ import chat.revolt.api.RevoltHttp
import chat.revolt.api.RevoltJson
import chat.revolt.api.schemas.Channel
import chat.revolt.screens.create.MAX_ADDABLE_PEOPLE_IN_GROUP
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 io.ktor.http.isSuccess
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
@ -37,4 +40,20 @@ suspend fun createGroupDM(name: String, members: List<String>): Channel {
}
return RevoltJson.decodeFromString(Channel.serializer(), response)
}
suspend fun removeMember(channelId: String, userId: String) {
val response = RevoltHttp.delete("/channels/$channelId/recipients/$userId")
if (!response.status.isSuccess()) {
throw Error(response.status.toString())
}
}
suspend fun addMember(channelId: String, userId: String) {
val response = RevoltHttp.put("/channels/$channelId/recipients/$userId")
if (!response.status.isSuccess()) {
throw Error(response.status.toString())
}
}

View File

@ -0,0 +1,151 @@
package chat.revolt.sheets
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
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.text.style.TextOverflow
import chat.revolt.R
import chat.revolt.api.RevoltAPI
import chat.revolt.api.routes.channel.removeMember
import chat.revolt.internals.Platform
import kotlinx.coroutines.launch
@Composable
fun ColumnScope.GroupDMMemberContextSheet(
userId: String,
channelId: String,
dismissSheet: suspend () -> Unit,
onRequestUpdateMembers: suspend () -> Unit
) {
val scope = rememberCoroutineScope()
val channel = RevoltAPI.channelCache[channelId]
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
LaunchedEffect(channel) {
if (channel == null) {
dismissSheet()
}
}
if (channel == null) return
if (channel.owner == RevoltAPI.selfId && userId != RevoltAPI.selfId) {
ListItem(
headlineContent = {
CompositionLocalProvider(value = LocalContentColor provides MaterialTheme.colorScheme.error) {
Text(
stringResource(
R.string.member_context_sheet_remove_from_channel,
channel.name ?: stringResource(R.string.unknown)
),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
},
leadingContent = {
CompositionLocalProvider(value = LocalContentColor provides MaterialTheme.colorScheme.error) {
Icon(
painter = painterResource(R.drawable.ic_account_cancel_24dp),
contentDescription = null
)
}
},
modifier = Modifier.clickable {
scope.launch {
removeMember(channelId, userId)
onRequestUpdateMembers()
dismissSheet()
}
}
)
}
// TODO replace with something useful (currently so that your sheet is not empty if you don't have permissions)
ListItem(
headlineContent = {
Text(stringResource(R.string.user_info_sheet_copy_id))
},
leadingContent = {
Icon(
painter = painterResource(R.drawable.ic_content_copy_id_24dp),
contentDescription = null
)
},
modifier = Modifier.clickable {
clipboardManager.setText(AnnotatedString(userId))
if (Platform.needsShowClipboardNotification()) {
Toast.makeText(
context,
context.getString(R.string.copied),
Toast.LENGTH_SHORT
).show()
}
}
)
}
@Composable
fun ColumnScope.ServerMemberContextSheet(
userId: String,
serverId: String,
channelId: String,
dismissSheet: suspend () -> Unit,
onRequestUpdateMembers: suspend () -> Unit
) {
val server = RevoltAPI.serverCache[serverId]
val channel = RevoltAPI.channelCache[channelId]
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
LaunchedEffect(server) {
if (server == null || channel == null) {
dismissSheet()
}
}
if (server == null || channel == null) return
// TODO add something useful (moderation actions)
// TODO replace with something useful (currently so that your sheet is not empty if you don't have permissions)
ListItem(
headlineContent = {
Text(stringResource(R.string.user_info_sheet_copy_id))
},
leadingContent = {
Icon(
painter = painterResource(R.drawable.ic_content_copy_id_24dp),
contentDescription = null
)
},
modifier = Modifier.clickable {
clipboardManager.setText(AnnotatedString(userId))
if (Platform.needsShowClipboardNotification()) {
Toast.makeText(
context,
context.getString(R.string.copied),
Toast.LENGTH_SHORT
).show()
}
}
)
}

View File

@ -4,7 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@ -209,8 +209,10 @@ fun MemberListSheet(
serverId: String? = null,
viewModel: MemberListSheetViewModel = hiltViewModel()
) {
var showUserContextSheet by remember { mutableStateOf(false) }
var userContextSheetTarget by remember { mutableStateOf("") }
var showUserInfoSheet by remember { mutableStateOf(false) }
var userInfoSheetTarget by remember { mutableStateOf("") }
var showMemberContextSheet by remember { mutableStateOf(false) }
var memberContextSheetTarget by remember { mutableStateOf("") }
// We use LaunchedEffect to make sure that this is called every time any of the users status changes
LaunchedEffect(RevoltAPI.userCache) {
@ -223,26 +225,64 @@ fun MemberListSheet(
}
}
if (showUserContextSheet) {
if (showUserInfoSheet) {
val userContextSheetState = rememberModalBottomSheetState()
ModalBottomSheet(
sheetState = userContextSheetState,
onDismissRequest = {
showUserContextSheet = false
showUserInfoSheet = false
}
) {
UserInfoSheet(
userId = userContextSheetTarget,
userId = userInfoSheetTarget,
serverId = serverId,
dismissSheet = {
userContextSheetState.hide()
showUserContextSheet = false
showUserInfoSheet = false
}
)
}
}
if (showMemberContextSheet) {
val memberContextSheetState = rememberModalBottomSheetState()
ModalBottomSheet(
sheetState = memberContextSheetState,
onDismissRequest = {
showMemberContextSheet = false
}
) {
if (serverId != null) {
ServerMemberContextSheet(
userId = memberContextSheetTarget,
serverId = serverId,
channelId = channelId,
onRequestUpdateMembers = {
viewModel.fetchServerMemberList(serverId, channelId)
},
dismissSheet = {
memberContextSheetState.hide()
showMemberContextSheet = false
}
)
} else {
GroupDMMemberContextSheet(
userId = memberContextSheetTarget,
channelId = channelId,
onRequestUpdateMembers = {
viewModel.fetchGroupMemberList(channelId)
},
dismissSheet = {
memberContextSheetState.hide()
showMemberContextSheet = false
}
)
}
}
}
if (viewModel.fullItemList.isEmpty()) {
Box(
modifier = Modifier
@ -281,10 +321,19 @@ fun MemberListSheet(
member = item.member,
serverId = serverId,
userId = item.member.id.user,
modifier = Modifier.clickable {
userContextSheetTarget = item.member.id.user
showUserContextSheet = true
}
modifier = Modifier
.combinedClickable(
onClick = {
userInfoSheetTarget = item.member.id.user
showUserInfoSheet = true
},
onClickLabel = stringResource(R.string.user_info_sheet_open),
onLongClick = {
memberContextSheetTarget = item.member.id.user
showMemberContextSheet = true
},
onLongClickLabel = stringResource(R.string.member_context_sheet_open)
)
)
}
@ -294,10 +343,18 @@ fun MemberListSheet(
member = null,
serverId = serverId,
userId = item.user.id,
modifier = Modifier.clickable {
userContextSheetTarget = item.user.id
showUserContextSheet = true
}
modifier = Modifier.combinedClickable(
onClick = {
userInfoSheetTarget = item.user.id
showUserInfoSheet = true
},
onClickLabel = stringResource(R.string.user_info_sheet_open),
onLongClick = {
memberContextSheetTarget = item.user.id
showMemberContextSheet = true
},
onLongClickLabel = stringResource(R.string.member_context_sheet_open)
)
)
}
}

View File

@ -304,6 +304,7 @@
<string name="server_context_sheet_actions_leave_silently">Leave Silently</string>
<string name="server_context_sheet_actions_report">Report</string>
<string name="user_info_sheet_open">Open user info</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>
@ -330,6 +331,9 @@
<string name="user_info_sheet_user_is_bot">This is a bot.</string>
<string name="user_info_sheet_user_is_bot_easter_egg">This is a bot. It has a plan.</string>
<string name="member_context_sheet_open">Open member options</string>
<string name="member_context_sheet_remove_from_channel">Remove from %1$s</string>
<string name="user_badge_developer">Developer</string>
<string name="user_badge_translator">Translator</string>
<string name="user_badge_supporter">Supporter</string>