From 48e63e549a88ac71bf88086a8abd2163f22d9dcd Mon Sep 17 00:00:00 2001 From: Infi Date: Fri, 25 Apr 2025 20:55:13 +0200 Subject: [PATCH] feat: new add server sheet with server creation Signed-off-by: Infi --- .../chat/revolt/api/routes/server/Server.kt | 31 + .../java/chat/revolt/api/schemas/Server.kt | 24 + .../composables/sheets/SheetSelection.kt | 61 ++ .../composables/vectorassets/CreateServer.kt | 79 +++ .../composables/vectorassets/JoinServer.kt | 54 ++ .../composables/vectorassets/NewServer.kt | 399 ++++++++++++ .../revolt/screens/chat/ChatRouterScreen.kt | 6 +- .../java/chat/revolt/sheets/AddServerSheet.kt | 582 ++++++++++++++---- app/src/main/res/values/strings.xml | 30 +- 9 files changed, 1129 insertions(+), 137 deletions(-) create mode 100644 app/src/main/java/chat/revolt/composables/sheets/SheetSelection.kt create mode 100644 app/src/main/java/chat/revolt/composables/vectorassets/CreateServer.kt create mode 100644 app/src/main/java/chat/revolt/composables/vectorassets/JoinServer.kt create mode 100644 app/src/main/java/chat/revolt/composables/vectorassets/NewServer.kt diff --git a/app/src/main/java/chat/revolt/api/routes/server/Server.kt b/app/src/main/java/chat/revolt/api/routes/server/Server.kt index 0965c374..5428400c 100644 --- a/app/src/main/java/chat/revolt/api/routes/server/Server.kt +++ b/app/src/main/java/chat/revolt/api/routes/server/Server.kt @@ -6,11 +6,14 @@ import chat.revolt.api.RevoltHttp import chat.revolt.api.RevoltJson import chat.revolt.api.api import chat.revolt.api.schemas.Member +import chat.revolt.api.schemas.ServerWithChannelObjects import chat.revolt.api.schemas.User import io.ktor.client.request.delete import io.ktor.client.request.get import io.ktor.client.request.parameter +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 kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException @@ -91,3 +94,31 @@ suspend fun leaveOrDeleteServer(serverId: String, leaveSilently: Boolean = false parameter("leave_silently", leaveSilently) } } + +@Serializable +data class ServerCreationBody( + val name: String, + val description: String? = null, + val nsfw: Boolean = false +) + +suspend fun createServer( + name: String, + description: String = "", + nsfw: Boolean = false +): ServerWithChannelObjects { + val body = ServerCreationBody(name, description, nsfw) + + val response = RevoltHttp.post("/servers/create".api()) { + setBody(RevoltJson.encodeToString(ServerCreationBody.serializer(), body)) + } + + try { + val error = RevoltJson.decodeFromString(RevoltError.serializer(), response.bodyAsText()) + throw Exception(error.type) + } catch (e: SerializationException) { + // Not an error + } + + return RevoltJson.decodeFromString(ServerWithChannelObjects.serializer(), response.bodyAsText()) +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/schemas/Server.kt b/app/src/main/java/chat/revolt/api/schemas/Server.kt index b97bd8d2..8bd38a38 100644 --- a/app/src/main/java/chat/revolt/api/schemas/Server.kt +++ b/app/src/main/java/chat/revolt/api/schemas/Server.kt @@ -100,3 +100,27 @@ data class EmojiParent( val type: String? = null, val id: String? = null ) + +/** + * Like [Server] but with complete channel objects instead of only IDs. + */ +@Serializable +data class ServerWithChannelObjects( + @SerialName("_id") + val id: String? = null, + val owner: String? = null, + val name: String? = null, + val description: String? = null, + val channels: List? = null, + val categories: List? = null, + @SerialName("system_messages") + val systemMessages: SystemMessages? = null, + val roles: Map? = null, + @SerialName("default_permissions") + val defaultPermissions: Long? = null, + val icon: AutumnResource? = null, + val banner: AutumnResource? = null, + val flags: Long? = null, + val analytics: Boolean? = null, + val discoverable: Boolean? = null +) \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/composables/sheets/SheetSelection.kt b/app/src/main/java/chat/revolt/composables/sheets/SheetSelection.kt new file mode 100644 index 00000000..636f82db --- /dev/null +++ b/app/src/main/java/chat/revolt/composables/sheets/SheetSelection.kt @@ -0,0 +1,61 @@ +package chat.revolt.composables.sheets + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp + +/** + * Sheet selection. Used when a modal sheet prompts a choice out of x options. + * The choices have an extended icon, a title and a description. + * An end-facing chevron is shown on the end side of the row. + */ +@Composable +fun SheetSelection( + icon: @Composable () -> Unit, + title: @Composable () -> Unit, + description: @Composable () -> Unit, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(MaterialTheme.shapes.medium) + .clickable(onClick = onClick) + .padding(16.dp) + ) { + icon() + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .weight(1f) + .padding(start = 8.dp) + .then(modifier) + ) { + ProvideTextStyle(MaterialTheme.typography.titleMedium) { + title() + } + ProvideTextStyle(MaterialTheme.typography.bodyMedium) { + description() + } + } + Icon( + imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/composables/vectorassets/CreateServer.kt b/app/src/main/java/chat/revolt/composables/vectorassets/CreateServer.kt new file mode 100644 index 00000000..5047f61a --- /dev/null +++ b/app/src/main/java/chat/revolt/composables/vectorassets/CreateServer.kt @@ -0,0 +1,79 @@ +package chat.revolt.composables.vectorassets + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val CreateServer: ImageVector + @Composable + get() { + if (_CreateServer != null) { + return _CreateServer!! + } + _CreateServer = ImageVector.Builder( + name = "CreateServer", + defaultWidth = 605.dp, + defaultHeight = 604.dp, + viewportWidth = 605f, + viewportHeight = 604f + ).apply { + path(fill = SolidColor(MaterialTheme.colorScheme.primaryContainer)) { + moveTo(270.33f, 36.02f) + curveTo(287.45f, 16.89f, 317.41f, 16.89f, 334.54f, 36.02f) + lineTo(374.1f, 80.21f) + curveTo(382.83f, 89.96f, 395.52f, 95.22f, 408.59f, 94.5f) + lineTo(467.8f, 91.22f) + curveTo(493.45f, 89.8f, 514.63f, 110.99f, 513.21f, 136.63f) + lineTo(509.94f, 195.85f) + curveTo(509.22f, 208.91f, 514.47f, 221.6f, 524.22f, 230.34f) + lineTo(568.41f, 269.89f) + curveTo(587.54f, 287.02f, 587.54f, 316.98f, 568.41f, 334.11f) + lineTo(524.22f, 373.67f) + curveTo(514.47f, 382.4f, 509.22f, 395.08f, 509.94f, 408.15f) + lineTo(513.21f, 467.37f) + curveTo(514.63f, 493.01f, 493.45f, 514.2f, 467.8f, 512.78f) + lineTo(408.59f, 509.5f) + curveTo(395.52f, 508.78f, 382.83f, 514.04f, 374.1f, 523.79f) + lineTo(334.54f, 567.97f) + curveTo(317.41f, 587.11f, 287.45f, 587.11f, 270.33f, 567.97f) + lineTo(230.77f, 523.79f) + curveTo(222.04f, 514.04f, 209.35f, 508.78f, 196.28f, 509.5f) + lineTo(137.07f, 512.78f) + curveTo(111.42f, 514.2f, 90.24f, 493.01f, 91.66f, 467.37f) + lineTo(94.93f, 408.15f) + curveTo(95.65f, 395.08f, 90.4f, 382.4f, 80.64f, 373.67f) + lineTo(36.46f, 334.11f) + curveTo(17.32f, 316.98f, 17.32f, 287.02f, 36.46f, 269.89f) + lineTo(80.64f, 230.34f) + curveTo(90.4f, 221.6f, 95.65f, 208.91f, 94.93f, 195.85f) + lineTo(91.66f, 136.63f) + curveTo(90.24f, 110.99f, 111.42f, 89.8f, 137.07f, 91.22f) + lineTo(196.28f, 94.5f) + curveTo(209.35f, 95.22f, 222.04f, 89.96f, 230.77f, 80.21f) + lineTo(270.33f, 36.02f) + close() + } + path(fill = SolidColor(MaterialTheme.colorScheme.onPrimaryContainer)) { + moveTo(285.02f, 404.6f) + verticalLineTo(199.4f) + horizontalLineTo(319.85f) + verticalLineTo(404.6f) + horizontalLineTo(285.02f) + close() + moveTo(199.83f, 319.41f) + verticalLineTo(284.59f) + horizontalLineTo(405.03f) + verticalLineTo(319.41f) + horizontalLineTo(199.83f) + close() + } + }.build() + + return _CreateServer!! + } + +@Suppress("ObjectPropertyName") +private var _CreateServer: ImageVector? = null \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/composables/vectorassets/JoinServer.kt b/app/src/main/java/chat/revolt/composables/vectorassets/JoinServer.kt new file mode 100644 index 00000000..5aa2a5fa --- /dev/null +++ b/app/src/main/java/chat/revolt/composables/vectorassets/JoinServer.kt @@ -0,0 +1,54 @@ +package chat.revolt.composables.vectorassets + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val JoinServer: ImageVector + @Composable + get() { + if (_JoinServer != null) { + return _JoinServer!! + } + _JoinServer = ImageVector.Builder( + name = "JoinServer", + defaultWidth = 518.dp, + defaultHeight = 338.dp, + viewportWidth = 518f, + viewportHeight = 338f + ).apply { + path(fill = SolidColor(MaterialTheme.colorScheme.secondaryContainer)) { + moveTo(196.04f, 208.6f) + curveTo(174.16f, 186.73f, 174.16f, 151.27f, 196.04f, 129.4f) + lineTo(308.76f, 16.68f) + curveTo(330.63f, -5.19f, 366.08f, -5.19f, 387.95f, 16.68f) + lineTo(500.68f, 129.4f) + curveTo(522.55f, 151.27f, 522.55f, 186.73f, 500.68f, 208.6f) + lineTo(387.95f, 321.32f) + curveTo(366.08f, 343.19f, 330.63f, 343.19f, 308.76f, 321.32f) + lineTo(196.04f, 208.6f) + close() + } + path(fill = SolidColor(MaterialTheme.colorScheme.onSurface)) { + moveTo(85.98f, 250.61f) + lineTo(67.19f, 231.99f) + lineTo(116.5f, 182.68f) + horizontalLineTo(0.96f) + verticalLineTo(155.32f) + horizontalLineTo(116.5f) + lineTo(67.19f, 106.09f) + lineTo(85.98f, 87.39f) + lineTo(167.59f, 169f) + lineTo(85.98f, 250.61f) + close() + } + }.build() + + return _JoinServer!! + } + +@Suppress("ObjectPropertyName") +private var _JoinServer: ImageVector? = null \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/composables/vectorassets/NewServer.kt b/app/src/main/java/chat/revolt/composables/vectorassets/NewServer.kt new file mode 100644 index 00000000..e9e2c520 --- /dev/null +++ b/app/src/main/java/chat/revolt/composables/vectorassets/NewServer.kt @@ -0,0 +1,399 @@ +package chat.revolt.composables.vectorassets + +import androidx.compose.foundation.Image +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +val NewServer: ImageVector + @Composable + get() { + if (_NewServer != null) { + return _NewServer!! + } + _NewServer = ImageVector.Builder( + name = "NewServer", + defaultWidth = 570.dp, + defaultHeight = 508.dp, + viewportWidth = 570f, + viewportHeight = 508f + ).apply { + path(fill = SolidColor(MaterialTheme.colorScheme.primaryContainer)) { + moveTo(131.17f, 166.37f) + lineTo(131.17f, 166.37f) + arcTo( + 131.03f, + 131.03f, + 0f, + isMoreThanHalf = false, + isPositiveArc = true, + 262.2f, + 297.4f + ) + lineTo(262.2f, 297.4f) + arcTo( + 131.03f, + 131.03f, + 0f, + isMoreThanHalf = false, + isPositiveArc = true, + 131.17f, + 428.43f + ) + lineTo(131.17f, 428.43f) + arcTo( + 131.03f, + 131.03f, + 0f, + isMoreThanHalf = false, + isPositiveArc = true, + 0.14f, + 297.4f + ) + lineTo(0.14f, 297.4f) + arcTo( + 131.03f, + 131.03f, + 0f, + isMoreThanHalf = false, + isPositiveArc = true, + 131.17f, + 166.37f + ) + close() + } + path(fill = SolidColor(MaterialTheme.colorScheme.onPrimaryContainer)) { + moveTo(87.36f, 357.23f) + lineTo(92.08f, 330.64f) + horizontalLineTo(65.48f) + lineTo(67.81f, 317.34f) + horizontalLineTo(94.4f) + lineTo(101.45f, 277.45f) + horizontalLineTo(74.86f) + lineTo(77.18f, 264.15f) + horizontalLineTo(103.78f) + lineTo(108.5f, 237.56f) + horizontalLineTo(121.79f) + lineTo(117.07f, 264.15f) + horizontalLineTo(156.96f) + lineTo(161.68f, 237.56f) + horizontalLineTo(174.98f) + lineTo(170.26f, 264.15f) + horizontalLineTo(196.85f) + lineTo(194.53f, 277.45f) + horizontalLineTo(167.93f) + lineTo(160.89f, 317.34f) + horizontalLineTo(187.48f) + lineTo(185.15f, 330.64f) + horizontalLineTo(158.56f) + lineTo(153.84f, 357.23f) + horizontalLineTo(140.54f) + lineTo(145.26f, 330.64f) + horizontalLineTo(105.37f) + lineTo(100.65f, 357.23f) + horizontalLineTo(87.36f) + close() + moveTo(114.75f, 277.45f) + lineTo(107.7f, 317.34f) + horizontalLineTo(147.59f) + lineTo(154.64f, 277.45f) + horizontalLineTo(114.75f) + close() + } + path(fill = SolidColor(MaterialTheme.colorScheme.secondaryContainer)) { + moveTo(369.52f, 314.28f) + lineTo(369.52f, 314.28f) + arcTo( + 96.66f, + 96.66f, + 0f, + isMoreThanHalf = false, + isPositiveArc = true, + 466.18f, + 410.94f + ) + lineTo(466.18f, 410.94f) + arcTo( + 96.66f, + 96.66f, + 0f, + isMoreThanHalf = false, + isPositiveArc = true, + 369.52f, + 507.6f + ) + lineTo(369.52f, 507.6f) + arcTo( + 96.66f, + 96.66f, + 0f, + isMoreThanHalf = false, + isPositiveArc = true, + 272.86f, + 410.94f + ) + lineTo(272.86f, 410.94f) + arcTo( + 96.66f, + 96.66f, + 0f, + isMoreThanHalf = false, + isPositiveArc = true, + 369.52f, + 314.28f + ) + close() + } + path(fill = SolidColor(MaterialTheme.colorScheme.onSecondaryContainer)) { + moveTo(389.14f, 441.79f) + verticalLineTo(451.6f) + horizontalLineTo(320.48f) + verticalLineTo(441.79f) + curveTo(320.48f, 441.79f, 320.48f, 422.18f, 354.81f, 422.18f) + curveTo(389.14f, 422.18f, 389.14f, 441.79f, 389.14f, 441.79f) + close() + moveTo(371.97f, 395.2f) + curveTo(371.97f, 391.81f, 370.97f, 388.49f, 369.08f, 385.67f) + curveTo(367.2f, 382.84f, 364.51f, 380.64f, 361.38f, 379.35f) + curveTo(358.24f, 378.05f, 354.79f, 377.71f, 351.46f, 378.37f) + curveTo(348.13f, 379.03f, 345.07f, 380.67f, 342.67f, 383.07f) + curveTo(340.27f, 385.47f, 338.64f, 388.52f, 337.97f, 391.86f) + curveTo(337.31f, 395.18f, 337.65f, 398.64f, 338.95f, 401.77f) + curveTo(340.25f, 404.91f, 342.45f, 407.59f, 345.27f, 409.48f) + curveTo(348.1f, 411.36f, 351.41f, 412.37f, 354.81f, 412.37f) + curveTo(359.36f, 412.37f, 363.73f, 410.56f, 366.95f, 407.34f) + curveTo(370.17f, 404.12f, 371.97f, 399.76f, 371.97f, 395.2f) + close() + moveTo(388.84f, 422.18f) + curveTo(391.86f, 424.51f, 394.33f, 427.48f, 396.07f, 430.86f) + curveTo(397.82f, 434.25f, 398.8f, 437.98f, 398.95f, 441.79f) + verticalLineTo(451.6f) + horizontalLineTo(418.57f) + verticalLineTo(441.79f) + curveTo(418.57f, 441.79f, 418.57f, 423.99f, 388.84f, 422.18f) + close() + moveTo(384.23f, 378.04f) + curveTo(380.86f, 378.02f, 377.56f, 379.03f, 374.77f, 380.93f) + curveTo(377.75f, 385.09f, 379.35f, 390.08f, 379.35f, 395.2f) + curveTo(379.35f, 400.32f, 377.75f, 405.31f, 374.77f, 409.48f) + curveTo(377.56f, 411.38f, 380.86f, 412.38f, 384.23f, 412.37f) + curveTo(388.79f, 412.37f, 393.15f, 410.56f, 396.37f, 407.34f) + curveTo(399.59f, 404.12f, 401.4f, 399.76f, 401.4f, 395.2f) + curveTo(401.4f, 390.65f, 399.59f, 386.29f, 396.37f, 383.07f) + curveTo(393.15f, 379.85f, 388.79f, 378.04f, 384.23f, 378.04f) + close() + } + path(fill = SolidColor(MaterialTheme.colorScheme.errorContainer)) { + moveTo(202.3f, 40.46f) + lineTo(202.3f, 40.46f) + arcTo( + 59.91f, + 59.91f, + 0f, + isMoreThanHalf = false, + isPositiveArc = true, + 262.2f, + 100.36f + ) + lineTo(262.2f, 100.36f) + arcTo( + 59.91f, + 59.91f, + 0f, + isMoreThanHalf = false, + isPositiveArc = true, + 202.3f, + 160.27f + ) + lineTo(202.3f, 160.27f) + arcTo( + 59.91f, + 59.91f, + 0f, + isMoreThanHalf = false, + isPositiveArc = true, + 142.39f, + 100.36f + ) + lineTo(142.39f, 100.36f) + arcTo( + 59.91f, + 59.91f, + 0f, + isMoreThanHalf = false, + isPositiveArc = true, + 202.3f, + 40.46f + ) + close() + } + path(fill = SolidColor(MaterialTheme.colorScheme.onErrorContainer)) { + moveTo(202.3f, 67.27f) + curveTo(203.91f, 67.27f, 205.46f, 67.91f, 206.6f, 69.05f) + curveTo(207.74f, 70.19f, 208.38f, 71.73f, 208.38f, 73.34f) + curveTo(208.38f, 75.59f, 207.16f, 77.57f, 205.34f, 78.6f) + verticalLineTo(82.46f) + horizontalLineTo(208.38f) + curveTo(214.02f, 82.46f, 219.43f, 84.71f, 223.42f, 88.7f) + curveTo(227.41f, 92.69f, 229.65f, 98.1f, 229.65f, 103.74f) + horizontalLineTo(232.69f) + curveTo(233.5f, 103.74f, 234.27f, 104.06f, 234.84f, 104.63f) + curveTo(235.41f, 105.2f, 235.73f, 105.97f, 235.73f, 106.78f) + verticalLineTo(115.9f) + curveTo(235.73f, 116.71f, 235.41f, 117.48f, 234.84f, 118.05f) + curveTo(234.27f, 118.62f, 233.5f, 118.94f, 232.69f, 118.94f) + horizontalLineTo(229.65f) + verticalLineTo(121.98f) + curveTo(229.65f, 123.59f, 229.01f, 125.14f, 227.87f, 126.28f) + curveTo(226.73f, 127.42f, 225.19f, 128.06f, 223.57f, 128.06f) + horizontalLineTo(181.02f) + curveTo(179.41f, 128.06f, 177.86f, 127.42f, 176.72f, 126.28f) + curveTo(175.58f, 125.14f, 174.94f, 123.59f, 174.94f, 121.98f) + verticalLineTo(118.94f) + horizontalLineTo(171.9f) + curveTo(171.1f, 118.94f, 170.32f, 118.62f, 169.75f, 118.05f) + curveTo(169.18f, 117.48f, 168.86f, 116.71f, 168.86f, 115.9f) + verticalLineTo(106.78f) + curveTo(168.86f, 105.97f, 169.18f, 105.2f, 169.75f, 104.63f) + curveTo(170.32f, 104.06f, 171.1f, 103.74f, 171.9f, 103.74f) + horizontalLineTo(174.94f) + curveTo(174.94f, 98.1f, 177.18f, 92.69f, 181.17f, 88.7f) + curveTo(185.16f, 84.71f, 190.57f, 82.46f, 196.22f, 82.46f) + horizontalLineTo(199.26f) + verticalLineTo(78.6f) + curveTo(197.43f, 77.57f, 196.22f, 75.59f, 196.22f, 73.34f) + curveTo(196.22f, 71.73f, 196.86f, 70.19f, 198f, 69.05f) + curveTo(199.14f, 67.91f, 200.68f, 67.27f, 202.3f, 67.27f) + close() + moveTo(188.62f, 100.7f) + curveTo(186.6f, 100.7f, 184.67f, 101.5f, 183.25f, 102.93f) + curveTo(181.82f, 104.35f, 181.02f, 106.29f, 181.02f, 108.3f) + curveTo(181.02f, 110.32f, 181.82f, 112.25f, 183.25f, 113.67f) + curveTo(184.67f, 115.1f, 186.6f, 115.9f, 188.62f, 115.9f) + curveTo(190.63f, 115.9f, 192.57f, 115.1f, 193.99f, 113.67f) + curveTo(195.42f, 112.25f, 196.22f, 110.32f, 196.22f, 108.3f) + curveTo(196.22f, 106.29f, 195.42f, 104.35f, 193.99f, 102.93f) + curveTo(192.57f, 101.5f, 190.63f, 100.7f, 188.62f, 100.7f) + close() + moveTo(215.98f, 100.7f) + curveTo(213.96f, 100.7f, 212.03f, 101.5f, 210.6f, 102.93f) + curveTo(209.18f, 104.35f, 208.38f, 106.29f, 208.38f, 108.3f) + curveTo(208.38f, 110.32f, 209.18f, 112.25f, 210.6f, 113.67f) + curveTo(212.03f, 115.1f, 213.96f, 115.9f, 215.98f, 115.9f) + curveTo(217.99f, 115.9f, 219.92f, 115.1f, 221.35f, 113.67f) + curveTo(222.77f, 112.25f, 223.57f, 110.32f, 223.57f, 108.3f) + curveTo(223.57f, 106.29f, 222.77f, 104.35f, 221.35f, 102.93f) + curveTo(219.92f, 101.5f, 217.99f, 100.7f, 215.98f, 100.7f) + close() + } + path(fill = SolidColor(MaterialTheme.colorScheme.tertiaryContainer)) { + moveTo(421.36f, 0.4f) + lineTo(421.36f, 0.4f) + arcTo( + 148.5f, + 148.5f, + 0f, + isMoreThanHalf = false, + isPositiveArc = true, + 569.86f, + 148.9f + ) + lineTo(569.86f, 148.9f) + arcTo( + 148.5f, + 148.5f, + 0f, + isMoreThanHalf = false, + isPositiveArc = true, + 421.36f, + 297.4f + ) + lineTo(421.36f, 297.4f) + arcTo( + 148.5f, + 148.5f, + 0f, + isMoreThanHalf = false, + isPositiveArc = true, + 272.86f, + 148.9f + ) + lineTo(272.86f, 148.9f) + arcTo( + 148.5f, + 148.5f, + 0f, + isMoreThanHalf = false, + isPositiveArc = true, + 421.36f, + 0.4f + ) + close() + } + path(fill = SolidColor(MaterialTheme.colorScheme.onTertiaryContainer)) { + moveTo(421.36f, 171.5f) + curveTo(427.47f, 171.5f, 432.67f, 169.24f, 437.26f, 164.8f) + curveTo(441.71f, 160.2f, 443.97f, 155f, 443.97f, 148.9f) + curveTo(443.97f, 142.8f, 441.71f, 137.6f, 437.26f, 133f) + curveTo(432.67f, 128.56f, 427.47f, 126.3f, 421.36f, 126.3f) + curveTo(415.26f, 126.3f, 410.06f, 128.56f, 405.47f, 133f) + curveTo(401.02f, 137.6f, 398.76f, 142.8f, 398.76f, 148.9f) + curveTo(398.76f, 155f, 401.02f, 160.2f, 405.47f, 164.8f) + curveTo(410.06f, 169.24f, 415.26f, 171.5f, 421.36f, 171.5f) + close() + moveTo(421.36f, 73.56f) + curveTo(442.08f, 73.56f, 459.79f, 81.09f, 474.48f, 95.78f) + curveTo(489.17f, 110.47f, 496.71f, 128.18f, 496.71f, 148.9f) + verticalLineTo(159.83f) + curveTo(496.71f, 167.36f, 494.07f, 173.76f, 489.17f, 179.04f) + curveTo(483.9f, 184.09f, 477.87f, 186.57f, 470.34f, 186.57f) + curveTo(461.3f, 186.57f, 453.84f, 182.81f, 448.19f, 175.27f) + curveTo(440.65f, 182.81f, 431.76f, 186.57f, 421.36f, 186.57f) + curveTo(411.04f, 186.57f, 402.15f, 182.81f, 394.69f, 175.57f) + curveTo(387.46f, 168.11f, 383.69f, 159.3f, 383.69f, 148.9f) + curveTo(383.69f, 138.58f, 387.46f, 129.69f, 394.69f, 122.23f) + curveTo(402.15f, 115f, 411.04f, 111.23f, 421.36f, 111.23f) + curveTo(431.76f, 111.23f, 440.58f, 115f, 448.04f, 122.23f) + curveTo(455.27f, 129.69f, 459.04f, 138.58f, 459.04f, 148.9f) + verticalLineTo(159.83f) + curveTo(459.04f, 162.91f, 460.24f, 165.63f, 462.5f, 167.96f) + curveTo(464.76f, 170.3f, 467.4f, 171.5f, 470.34f, 171.5f) + curveTo(473.5f, 171.5f, 476.14f, 170.3f, 478.4f, 167.96f) + curveTo(480.66f, 165.63f, 481.64f, 162.91f, 481.64f, 159.83f) + verticalLineTo(148.9f) + curveTo(481.64f, 132.4f, 475.84f, 118.24f, 463.93f, 106.33f) + curveTo(452.03f, 94.43f, 437.86f, 88.62f, 421.36f, 88.62f) + curveTo(404.86f, 88.62f, 390.7f, 94.43f, 378.8f, 106.33f) + curveTo(366.89f, 118.24f, 361.09f, 132.4f, 361.09f, 148.9f) + curveTo(361.09f, 165.4f, 366.89f, 179.57f, 378.8f, 191.47f) + curveTo(390.7f, 203.38f, 404.86f, 209.18f, 421.36f, 209.18f) + horizontalLineTo(459.04f) + verticalLineTo(224.24f) + horizontalLineTo(421.36f) + curveTo(400.64f, 224.24f, 382.94f, 216.71f, 368.25f, 202.02f) + curveTo(353.55f, 187.33f, 346.02f, 169.62f, 346.02f, 148.9f) + curveTo(346.02f, 128.18f, 353.55f, 110.47f, 368.25f, 95.78f) + curveTo(382.94f, 81.09f, 400.64f, 73.56f, 421.36f, 73.56f) + close() + } + }.build() + + return _NewServer!! + } + +@Suppress("ObjectPropertyName") +private var _NewServer: ImageVector? = null + + +@Preview(showBackground = true) +@Composable +fun NewServerPreview() { + Image( + imageVector = NewServer, + contentDescription = null + ) +} diff --git a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt index 2ab590a7..007beff3 100644 --- a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt @@ -517,7 +517,11 @@ fun ChatRouterScreen( showAddServerSheet = false } ) { - AddServerSheet() + AddServerSheet( + onDismiss = { + showAddServerSheet = false + } + ) } } diff --git a/app/src/main/java/chat/revolt/sheets/AddServerSheet.kt b/app/src/main/java/chat/revolt/sheets/AddServerSheet.kt index 08ad1bae..3e57496c 100644 --- a/app/src/main/java/chat/revolt/sheets/AddServerSheet.kt +++ b/app/src/main/java/chat/revolt/sheets/AddServerSheet.kt @@ -2,165 +2,493 @@ package chat.revolt.sheets import android.content.Intent import android.util.Log -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ExitToApp -import androidx.compose.material.icons.filled.Build -import androidx.compose.material3.AlertDialog +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Button import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField 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.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.net.toUri import chat.revolt.R import chat.revolt.activities.InviteActivity import chat.revolt.api.REVOLT_APP -import chat.revolt.composables.generic.FormTextField -import chat.revolt.composables.generic.SheetButton -import chat.revolt.composables.generic.SheetHeaderPadding +import chat.revolt.api.routes.server.createServer +import chat.revolt.callbacks.Action +import chat.revolt.callbacks.ActionChannel +import chat.revolt.composables.sheets.SheetSelection +import chat.revolt.composables.vectorassets.CreateServer +import chat.revolt.composables.vectorassets.JoinServer +import chat.revolt.composables.vectorassets.NewServer +import chat.revolt.material.EasingTokens +import chat.revolt.screens.chat.ChatRouterDestination +import chat.revolt.ui.theme.FragmentMono +import kotlinx.coroutines.launch +import logcat.asLog +import logcat.logcat + +enum class AddServerSheetStep(val animationValue: Int) { + Initial(0), + JoinFromInvite(1), + CreateServer(1) +} @Composable -fun AddServerSheet() { +fun AddServerSheet(onDismiss: () -> Unit) { + var currentStep by remember { mutableStateOf(AddServerSheetStep.Initial) } val context = LocalContext.current - - val joinFromInviteModalOpen = remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() Column( modifier = Modifier .verticalScroll(rememberScrollState()) ) { - if (joinFromInviteModalOpen.value) { - JoinFromInviteModal( - onDismiss = { - joinFromInviteModalOpen.value = false + AnimatedContent( + currentStep, + transitionSpec = { + if (targetState.animationValue > initialState.animationValue) { + (slideInHorizontally( + animationSpec = tween(300, 0, EasingTokens.EmphasizedDecelerate), + initialOffsetX = { fullWidth -> fullWidth } + ) + fadeIn( + animationSpec = tween(300, 0, EasingTokens.EmphasizedDecelerate) + )) togetherWith (slideOutHorizontally( + animationSpec = tween(300, 0, EasingTokens.EmphasizedDecelerate), + targetOffsetX = { fullWidth -> -fullWidth } + ) + fadeOut( + animationSpec = tween(300, 0, EasingTokens.EmphasizedDecelerate) + )) + } else { + (slideInHorizontally( + animationSpec = tween(300, 0, EasingTokens.EmphasizedDecelerate), + initialOffsetX = { fullWidth -> -fullWidth } + ) + fadeIn( + animationSpec = tween(300, 0, EasingTokens.EmphasizedDecelerate) + )) togetherWith (slideOutHorizontally( + animationSpec = tween(300, 0, EasingTokens.EmphasizedDecelerate), + targetOffsetX = { fullWidth -> fullWidth } + ) + fadeOut( + animationSpec = tween(300, 0, EasingTokens.EmphasizedDecelerate) + )) } + }, + contentAlignment = Alignment.BottomCenter, + modifier = Modifier.animateContentSize( + animationSpec = tween(150, 0, EasingTokens.EmphasizedDecelerate) ) - } - - SheetHeaderPadding { - Text( - text = stringResource(id = R.string.add_server_sheet_title), - style = MaterialTheme.typography.headlineSmall - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - SheetButton( - headlineContent = { - Text(stringResource(id = R.string.add_server_sheet_join_by_invite)) - }, - leadingContent = { - Icon( - imageVector = Icons.AutoMirrored.Default.ExitToApp, - contentDescription = null - ) - }, - onClick = { - joinFromInviteModalOpen.value = true - } - ) - - SheetButton( - headlineContent = { - Text(stringResource(id = R.string.add_server_sheet_create_new)) - }, - leadingContent = { - Icon( - imageVector = Icons.Default.Build, - contentDescription = null - ) - }, - onClick = { - Toast.makeText( - context, - context.getString( - R.string.add_server_sheet_create_new_modal_under_construction - ), - Toast.LENGTH_SHORT - ).show() - } - ) - } - - -} - -@Composable -fun JoinFromInviteModal(onDismiss: () -> Unit) { - val context = LocalContext.current - - val inviteCode = remember { mutableStateOf("") } - - val inviteActivityResult = rememberLauncherForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - Log.d("InviteActivity", "Result: $result") - } - - AlertDialog( - onDismissRequest = onDismiss, - title = { - Text(text = stringResource(id = R.string.add_server_sheet_join_by_invite_modal_title)) - }, - text = { - Column { - Text( - text = stringResource( - id = R.string.add_server_sheet_join_by_invite_modal_description - ) - ) - Spacer(modifier = Modifier.height(8.dp)) - FormTextField( - label = stringResource( - id = R.string.add_server_sheet_join_by_invite_modal_hint - ), - value = inviteCode.value, - onChange = { - inviteCode.value = it - } - ) - } - }, - confirmButton = { - TextButton( - onClick = { - val intent = Intent(context, InviteActivity::class.java) - intent.data = if (inviteCode.value.startsWith("https://")) { - inviteCode.value.toUri() - } else { - "https://$REVOLT_APP/invite/${inviteCode.value}".toUri() - } - inviteActivityResult.launch(intent) - } + ) { step -> + Box( + modifier = Modifier.padding(16.dp) ) { - Text( - text = stringResource(id = R.string.add_server_sheet_join_by_invite_modal_join) - ) - } - }, - dismissButton = { - TextButton( - onClick = { - onDismiss() + if (step == AddServerSheetStep.Initial) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + imageVector = NewServer, + contentDescription = null, + modifier = Modifier + .fillMaxWidth(0.5f) + ) + + Text( + text = stringResource(id = R.string.add_server_sheet_step_0_title), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Text( + text = stringResource(id = R.string.add_server_sheet_step_0_description), + style = MaterialTheme.typography.bodyMedium, + color = LocalContentColor.current.copy(alpha = 0.7f), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + SheetSelection( + icon = { + Image( + imageVector = JoinServer, + contentDescription = null, + modifier = Modifier + .size(48.dp) + ) + }, + title = { + Text( + text = stringResource(id = R.string.add_server_sheet_step_0_join) + ) + }, + description = { + Text( + text = stringResource(id = R.string.add_server_sheet_step_0_join_description) + ) + }, + ) { + currentStep = AddServerSheetStep.JoinFromInvite + } + + SheetSelection( + icon = { + Image( + imageVector = CreateServer, + contentDescription = null, + modifier = Modifier + .size(48.dp) + ) + }, + title = { + Text( + text = stringResource(id = R.string.add_server_sheet_step_0_create) + ) + }, + description = { + Text( + text = stringResource(id = R.string.add_server_sheet_step_0_create_description) + ) + }, + ) { + currentStep = AddServerSheetStep.CreateServer + } + } + } else if (step == AddServerSheetStep.JoinFromInvite) { + val inviteState = rememberTextFieldState() + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = stringResource(id = R.string.add_server_sheet_step_1j_title), + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + IconButton( + onClick = { + currentStep = AddServerSheetStep.Initial + } + ) { + Icon( + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = stringResource(id = R.string.back), + ) + } + } + + Text( + text = stringResource(id = R.string.add_server_sheet_step_1j_description), + style = MaterialTheme.typography.bodyMedium, + color = LocalContentColor.current.copy(alpha = 0.7f), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + + Text( + text = stringResource(id = R.string.add_server_sheet_step_1j_examples_heading), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(id = R.string.add_server_sheet_step_1j_example_1), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + fontFamily = FragmentMono, + modifier = Modifier + .clip( + MaterialTheme.shapes.small.copy( + topStart = MaterialTheme.shapes.large.topStart, + topEnd = MaterialTheme.shapes.large.topEnd, + ) + ) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + .padding(16.dp) + .fillMaxWidth() + ) + Text( + text = stringResource(id = R.string.add_server_sheet_step_1j_example_2), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + fontFamily = FragmentMono, + modifier = Modifier + .clip( + MaterialTheme.shapes.small + ) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + .padding(16.dp) + .fillMaxWidth() + ) + Text( + text = stringResource(id = R.string.add_server_sheet_step_1j_example_3), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + fontFamily = FragmentMono, + modifier = Modifier + .clip( + MaterialTheme.shapes.small.copy( + bottomStart = MaterialTheme.shapes.large.bottomStart, + bottomEnd = MaterialTheme.shapes.large.bottomEnd, + ) + ) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + .padding(16.dp) + .fillMaxWidth() + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + TextField( + state = inviteState, + label = { + Text(stringResource(R.string.add_server_sheet_step_1j_label)) + }, + lineLimits = TextFieldLineLimits.SingleLine, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp, horizontal = 8.dp) + ) + + Button( + onClick = { + val intent = Intent(context, InviteActivity::class.java) + intent.data = if (inviteState.text.startsWith("https://")) { + try { + inviteState.text.toString().toUri() + } catch (e: Exception) { + Log.e( + "AddServerSheet", + "Invalid URL: ${inviteState.text}", + e + ) + return@Button + } + } else { + "https://$REVOLT_APP/invite/${inviteState.text}".toUri() + } + context.startActivity(intent) + + onDismiss() + }, + modifier = Modifier + .fillMaxWidth() + ) { + Text( + text = stringResource(id = R.string.add_server_sheet_step_1j_join) + ) + } + } + } else if (step == AddServerSheetStep.CreateServer) { + val serverNameState = rememberTextFieldState() + val serverNameIsBlank = remember(serverNameState.text) { + serverNameState.text.isBlank() + } + + var serverNameRangeError by remember { mutableStateOf(false) } + var serverCreationError by remember { mutableStateOf(false) } + + LaunchedEffect(serverNameState.text) { + if (serverNameState.text.length > 32) { + serverNameRangeError = true + } else { + serverNameRangeError = false + } + } + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = stringResource(id = R.string.add_server_sheet_step_1c_title), + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + IconButton( + onClick = { + currentStep = AddServerSheetStep.Initial + } + ) { + Icon( + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = stringResource(id = R.string.back), + ) + } + } + + Text( + text = stringResource(id = R.string.add_server_sheet_step_1c_description), + style = MaterialTheme.typography.bodyMedium, + color = LocalContentColor.current.copy(alpha = 0.7f), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + val outline = MaterialTheme.colorScheme.outline + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Canvas(Modifier.size(80.dp)) { + val stroke = Stroke( + width = 4.dp.toPx(), + pathEffect = PathEffect.dashPathEffect( + floatArrayOf(30f, 40f), + 0f + ) + ) + + drawCircle( + color = outline, + style = stroke, + radius = size.minDimension / 2 - stroke.width / 2 + ) + } + Text( + text = when { + serverNameIsBlank -> stringResource(R.string.add_server_sheet_step_1c_name_placeholder) + else -> serverNameState.text.toString() + }, + style = MaterialTheme.typography.bodyLarge, + color = LocalContentColor.current.copy( + alpha = when { + serverNameIsBlank -> 0.5f + else -> 1f + } + ), + fontWeight = when { + serverNameIsBlank -> FontWeight.Medium + else -> FontWeight.Bold + }, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth() + ) + } + + TextField( + state = serverNameState, + label = { + Text(stringResource(R.string.add_server_sheet_step_1c_name)) + }, + lineLimits = TextFieldLineLimits.SingleLine, + supportingText = { + Text( + when { + serverCreationError -> stringResource(R.string.add_server_sheet_step_1c_error) + serverNameRangeError -> stringResource( + R.string.add_server_sheet_step_1c_name_error_range, + 32 + ) + + else -> "" + } + ) + }, + isError = serverNameRangeError || serverCreationError, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp, horizontal = 8.dp) + ) + + Button( + onClick = { + serverCreationError = false + + scope.launch { + try { + val server = createServer(serverNameState.text.toString()) + + // Backend should've already created a channel for us to go to + server.channels?.first()?.id?.let { + ActionChannel.send( + Action.ChatNavigate( + ChatRouterDestination.Channel(it) + ) + ) + } + + onDismiss() + } catch (e: Exception) { + serverCreationError = true + logcat { "Error creating server: ${e.asLog()}" } + } + } + }, + enabled = !serverNameIsBlank && !serverNameRangeError, + modifier = Modifier + .fillMaxWidth() + ) { + Text( + text = stringResource(id = R.string.add_server_sheet_step_1c_create) + ) + } + } } - ) { - Text(text = stringResource(id = R.string.cancel)) } } - ) + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c55488c7..a921319b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -435,15 +435,27 @@ sus It\'s Morbin Time - Add a server - Join by invite code or link - Invite code or link - Enter a link like rvlt.gg/Testers or an invite code like Testers - Invite code or link - Join - Create a new server - Create a new server - This feature is currently under construction. + Add Server + A server is where the magic happens. Chat with your friends, share memes, build communities, and more. + Join by invite code or link + You already have an invite code to join an existing server. + Create a new server + You want to create a completely new server from scratch. + Join with an Invite + If you have an invite code or link, you can join a server right now. + Examples of invite codes include: + rvlt.gg/Testers + Testers + app.revolt.chat/invite/Testers + Invite code or link + Join + Create a Server + You can always customise the name or icon and invite your friends later. + Server Name + My Server + Server name can be up to %1$d characters in length. + An error occurred while creating your server. Please try again later. + Create Discover Revolt