diff --git a/.idea/.gitignore b/.idea/.gitignore
index 8397494f..6d73431e 100644
--- a/.idea/.gitignore
+++ b/.idea/.gitignore
@@ -6,3 +6,5 @@
/other.xml
# GitHub Copilot persisted chat sessions
/copilot/chatSessions
+# User-specific files
+/deploymentTargetSelector.xml
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
deleted file mode 100644
index b268ef36..00000000
--- a/.idea/deploymentTargetSelector.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/api/routes/channel/GroupDM.kt b/app/src/main/java/chat/revolt/api/routes/channel/GroupDM.kt
index 4419bfef..14b6752e 100644
--- a/app/src/main/java/chat/revolt/api/routes/channel/GroupDM.kt
+++ b/app/src/main/java/chat/revolt/api/routes/channel/GroupDM.kt
@@ -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): 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())
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/sheets/MemberContextSheet.kt b/app/src/main/java/chat/revolt/sheets/MemberContextSheet.kt
new file mode 100644
index 00000000..188ba351
--- /dev/null
+++ b/app/src/main/java/chat/revolt/sheets/MemberContextSheet.kt
@@ -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()
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/sheets/MemberListSheet.kt b/app/src/main/java/chat/revolt/sheets/MemberListSheet.kt
index 9b57739e..1864d201 100644
--- a/app/src/main/java/chat/revolt/sheets/MemberListSheet.kt
+++ b/app/src/main/java/chat/revolt/sheets/MemberListSheet.kt
@@ -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)
+ )
)
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 816e42e8..4d305ede 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -304,6 +304,7 @@
Leave Silently
Report
+ Open user info
Can\'t resolve this user
This user may have been deleted or you may not have permission to view them.
Bio
@@ -330,6 +331,9 @@
This is a bot.
This is a bot. It has a plan.
+ Open member options
+ Remove from %1$s
+
Developer
Translator
Supporter