feat: invite screen

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2023-12-10 01:55:08 +01:00
parent 0a5676fca6
commit b68267c1e7
4 changed files with 243 additions and 1 deletions

View File

@ -17,6 +17,7 @@ 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.SerialName
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.ListSerializer
@ -78,7 +79,19 @@ data class EditMessageBody(
val content: String?
)
suspend fun sendMessage(
@kotlinx.serialization.Serializable
data class CreateInviteResponse(
val type: String,
@SerialName("_id")
val id: String,
val server: String,
val creator: String,
val channel: String,
)
suspend
fun sendMessage(
channelId: String,
content: String,
nonce: String? = ULID.makeNext(),
@ -143,3 +156,13 @@ suspend fun fetchGroupParticipants(channelId: String): List<User> {
response
)
}
suspend fun createInvite(channelId: String): CreateInviteResponse {
val response = RevoltHttp.post("/channels/$channelId/invites")
.bodyAsText()
val error = RevoltJson.decodeFromString(RevoltError.serializer(), response)
if (error.type != "Server") throw Error(error.type)
return RevoltJson.decodeFromString(CreateInviteResponse.serializer(), response)
}

View File

@ -0,0 +1,195 @@
package chat.revolt.screens.chat.dialogs
import android.net.Uri
import android.widget.Toast
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
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.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import chat.revolt.R
import chat.revolt.api.REVOLT_INVITES
import chat.revolt.api.RevoltAPI
import chat.revolt.api.routes.channel.createInvite
import chat.revolt.internals.Platform
private val inviteChars = ('a'..'z') + ('A'..'Z') + ('0'..'9')
private fun placeholderInviteCode(): String {
return (1..8)
.map { inviteChars.random() }
.joinToString("")
}
data class InviteCodeChar(
val char: Char,
val isActual: Boolean
)
operator fun InviteCodeChar.compareTo(other: InviteCodeChar): Int {
return char.compareTo(other.char)
}
@Composable
fun InviteDialog(channelId: String, onDismissRequest: () -> Unit) {
val channel = RevoltAPI.channelCache[channelId]
if (channel == null) {
onDismissRequest()
return
}
var isActual by remember { mutableStateOf(false) }
var inviteCode by remember { mutableStateOf(placeholderInviteCode()) }
val invitePrefixOpacity by animateFloatAsState(
targetValue = if (isActual) 1f else 0f,
label = "Invite prefix opacity"
)
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
LaunchedEffect(Unit) {
try {
val invite = createInvite(channelId)
isActual = true
inviteCode = invite.id
} catch (e: Error) {
isActual = true
inviteCode = "error"
}
}
BoxWithConstraints {
Column(
modifier = Modifier
.clip(MaterialTheme.shapes.large)
.background(MaterialTheme.colorScheme.surface)
.padding(24.dp)
.width(maxWidth * 0.85f)
.heightIn(max = maxHeight * 0.85f)
) {
Text(
stringResource(
R.string.invite_dialog_header,
channel.name ?: stringResource(R.string.unknown)
),
style = MaterialTheme.typography.headlineMedium
)
Spacer(Modifier.height(16.dp))
Text(
(Uri.parse(REVOLT_INVITES).host ?: "rvlt.gg") + "/",
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.alpha(invitePrefixOpacity)
)
Row(
modifier = Modifier
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
inviteCode
.map { InviteCodeChar(it, isActual) }
.forEach {
AnimatedContent(
targetState = it,
transitionSpec = {
if (targetState > initialState) {
slideInVertically { -it } togetherWith slideOutVertically { it }
} else {
slideInVertically { it } togetherWith slideOutVertically { -it }
}
},
label = "Invite code char"
) { state ->
Text(
state.char.toString(),
style = MaterialTheme.typography.displayLarge,
fontFamily = FontFamily(Font(R.font.jetbrainsmono_regular)),
modifier = Modifier
.alpha(if (state.isActual) 1f else 0f)
)
}
}
}
Spacer(Modifier.height(16.dp))
Text(
stringResource(R.string.invite_dialog_description),
style = MaterialTheme.typography.bodyMedium
)
Spacer(Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = {
onDismissRequest()
}) {
Text(stringResource(R.string.invite_dialog_close))
}
Spacer(Modifier.width(8.dp))
Button(onClick = {
clipboardManager.setText(AnnotatedString.Builder().apply {
append(REVOLT_INVITES)
append("/")
append(inviteCode)
}.toAnnotatedString())
if (Platform.needsShowClipboardNotification()) {
Toast.makeText(
context,
context.getString(R.string.copied),
Toast.LENGTH_SHORT
).show()
}
}) {
Text(stringResource(R.string.invite_dialog_copy))
}
}
}
}
}

View File

@ -28,6 +28,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import chat.revolt.R
import chat.revolt.api.RevoltAPI
import chat.revolt.api.internals.ChannelUtils
@ -37,12 +38,14 @@ import chat.revolt.api.internals.has
import chat.revolt.api.schemas.ChannelType
import chat.revolt.components.generic.SheetClickable
import chat.revolt.components.screens.chat.ChannelSheetHeader
import chat.revolt.screens.chat.dialogs.InviteDialog
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChannelInfoSheet(channelId: String) {
val channel = RevoltAPI.channelCache[channelId]
var memberListSheetShown by remember { mutableStateOf(false) }
var inviteDialogShown by remember { mutableStateOf(false) }
if (memberListSheetShown) {
val memberListSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
@ -60,6 +63,21 @@ fun ChannelInfoSheet(channelId: String) {
}
}
if (inviteDialogShown) {
Dialog(
onDismissRequest = {
inviteDialogShown = false
}
) {
InviteDialog(
channelId = channelId,
onDismissRequest = {
inviteDialogShown = false
}
)
}
}
if (channel == null) {
Box(
modifier = Modifier
@ -167,6 +185,7 @@ fun ChannelInfoSheet(channelId: String) {
)
}
) {
inviteDialogShown = true
}
}

View File

@ -239,6 +239,11 @@
<string name="channel_info_sheet_options_add">Add members</string>
<string name="channel_info_sheet_options_notifications_manage">Manage notifications</string>
<string name="invite_dialog_header">Invite to #%1$s</string>
<string name="invite_dialog_description">Send this link to invite people to this channel.</string>
<string name="invite_dialog_copy">Copy</string>
<string name="invite_dialog_close">Close</string>
<string name="message_context_sheet_actions_copy">Copy</string>
<string name="message_context_sheet_actions_copy_failed_empty">Message is empty, nothing to copy</string>
<string name="message_context_sheet_actions_reply">Reply</string>