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