From 2721d74c0467d53ce528957d90ff897d573161d0 Mon Sep 17 00:00:00 2001 From: Infi Date: Sat, 26 Jul 2025 07:09:23 +0200 Subject: [PATCH 1/6] chore: update metadata Signed-off-by: Infi --- .idea/codeStyles/Project.xml | 35 +++++++++++++++++++++++++++++++++++ .idea/deviceManager.xml | 13 +++++++++++++ .idea/misc.xml | 2 ++ .idea/valkyrie_settings.xml | 2 +- 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 .idea/deviceManager.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 7643783a..7f70f7ef 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,5 +1,40 @@ + + + diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 00000000..91f95584 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index bfb8555a..7542ef6d 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,3 +1,4 @@ + + diff --git a/.idea/valkyrie_settings.xml b/.idea/valkyrie_settings.xml index ae129850..237dcfcd 100644 --- a/.idea/valkyrie_settings.xml +++ b/.idea/valkyrie_settings.xml @@ -2,6 +2,6 @@ \ No newline at end of file From a92a07e2ed4788edb345d8c8207b108e95f8aad0 Mon Sep 17 00:00:00 2001 From: Infi Date: Sat, 26 Jul 2025 07:12:29 +0200 Subject: [PATCH 2/6] feat: geo support Signed-off-by: Infi --- .../chat/revolt/activities/MainActivity.kt | 13 + .../api/routes/microservices/geo/Geo.kt | 35 + .../revolt/api/settings/GeoStateProvider.kt | 18 + .../composables/screens/chat/ChannelIcon.kt | 2 +- .../screens/chat/drawer/ChannelSideDrawer.kt | 13 +- .../composables/vectorassets/GeoGateUX.kt | 92 ++ .../chat/views/channel/ChannelScreen.kt | 1124 +++++++++-------- .../views/channel/ChannelScreenGeoGate.kt | 81 ++ .../views/channel/ChannelScreenViewModel.kt | 6 + .../main/res/drawable/icn_grid_3x3_24dp.xml | 10 + .../res/drawable/icn_grid_3x3_off_24dp.xml | 10 + app/src/main/res/values/strings.xml | 6 + 12 files changed, 851 insertions(+), 559 deletions(-) create mode 100644 app/src/main/java/chat/revolt/api/routes/microservices/geo/Geo.kt create mode 100644 app/src/main/java/chat/revolt/api/settings/GeoStateProvider.kt create mode 100644 app/src/main/java/chat/revolt/composables/vectorassets/GeoGateUX.kt create mode 100644 app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenGeoGate.kt create mode 100644 app/src/main/res/drawable/icn_grid_3x3_24dp.xml create mode 100644 app/src/main/res/drawable/icn_grid_3x3_off_24dp.xml diff --git a/app/src/main/java/chat/revolt/activities/MainActivity.kt b/app/src/main/java/chat/revolt/activities/MainActivity.kt index 153b10c6..161db1fd 100644 --- a/app/src/main/java/chat/revolt/activities/MainActivity.kt +++ b/app/src/main/java/chat/revolt/activities/MainActivity.kt @@ -79,10 +79,12 @@ import chat.revolt.api.HitRateLimitException import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltHttp import chat.revolt.api.api +import chat.revolt.api.routes.microservices.geo.queryGeo import chat.revolt.api.routes.microservices.health.healthCheck import chat.revolt.api.routes.onboard.needsOnboarding import chat.revolt.api.schemas.HealthNotice import chat.revolt.api.settings.Experiments +import chat.revolt.api.settings.GeoStateProvider import chat.revolt.api.settings.LoadedSettings import chat.revolt.api.settings.SyncedSettings import chat.revolt.composables.generic.HealthAlert @@ -184,6 +186,17 @@ class MainActivityViewModel @Inject constructor( Experiments.hydrateWithKv() Log.d("MainActivity", "Performing health check") doHealthCheck() + Log.d("MainActivity", "Performing update geo state") + updateGeoState() + } + } + + private suspend fun updateGeoState() { + try { + Log.d("MainActivity", "Querying geo state") + GeoStateProvider.updateGeoState(queryGeo()) + } catch (e: Exception) { + Log.e("MainActivity", "Failed to query geo state", e) } } diff --git a/app/src/main/java/chat/revolt/api/routes/microservices/geo/Geo.kt b/app/src/main/java/chat/revolt/api/routes/microservices/geo/Geo.kt new file mode 100644 index 00000000..a54ea4b4 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/routes/microservices/geo/Geo.kt @@ -0,0 +1,35 @@ +package chat.revolt.api.routes.microservices.geo + +import chat.revolt.api.HitRateLimitException +import chat.revolt.api.RevoltHttp +import chat.revolt.api.RevoltJson +import chat.revolt.api.buildUserAgent +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpStatusCode +import kotlinx.serialization.Serializable + +@Serializable +data class GeoResponse( + val countryCode: String, + val isAgeRestrictedGeo: Boolean, +) + +suspend fun queryGeo(): GeoResponse { + try { + val response = RevoltHttp.get("https://geo.revolt.chat/?client=android") { + header("User-Agent", buildUserAgent("Ktor queryGeo")) + } + + if (response.status == HttpStatusCode.OK) { + return RevoltJson.decodeFromString(response.bodyAsText()) + } else throw Exception("Failed to query geo: ${response.status.value} ${response.status.description}") + } catch (e: Exception) { + throw Exception("Failed to query geo: ${e.message}", e).also { + if (e is HitRateLimitException) { + throw e + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/settings/GeoStateProvider.kt b/app/src/main/java/chat/revolt/api/settings/GeoStateProvider.kt new file mode 100644 index 00000000..47f50b35 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/settings/GeoStateProvider.kt @@ -0,0 +1,18 @@ +package chat.revolt.api.settings + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import chat.revolt.api.routes.microservices.geo.GeoResponse + +object GeoStateProvider { + var geoState by mutableStateOf(null) + private set + + fun updateGeoState(newGeoState: GeoResponse?) { + checkNotNull(newGeoState) { "You shall not unset this value" } + check(if (geoState?.isAgeRestrictedGeo == true) newGeoState.isAgeRestrictedGeo else true) { "You shall not apply a laxer value" } + + geoState = newGeoState + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/composables/screens/chat/ChannelIcon.kt b/app/src/main/java/chat/revolt/composables/screens/chat/ChannelIcon.kt index 9713296a..59e00931 100644 --- a/app/src/main/java/chat/revolt/composables/screens/chat/ChannelIcon.kt +++ b/app/src/main/java/chat/revolt/composables/screens/chat/ChannelIcon.kt @@ -16,7 +16,7 @@ fun ChannelIcon(channelType: ChannelType, modifier: Modifier = Modifier) { when (channelType) { ChannelType.TextChannel -> { Icon( - painter = painterResource(R.drawable.icn_tag_24dp), + painter = painterResource(R.drawable.icn_grid_3x3_24dp), contentDescription = stringResource(R.string.channel_text), modifier = modifier ) diff --git a/app/src/main/java/chat/revolt/composables/screens/chat/drawer/ChannelSideDrawer.kt b/app/src/main/java/chat/revolt/composables/screens/chat/drawer/ChannelSideDrawer.kt index 5e5cb176..c78fb7c4 100644 --- a/app/src/main/java/chat/revolt/composables/screens/chat/drawer/ChannelSideDrawer.kt +++ b/app/src/main/java/chat/revolt/composables/screens/chat/drawer/ChannelSideDrawer.kt @@ -87,6 +87,7 @@ import chat.revolt.api.schemas.ChannelType import chat.revolt.api.schemas.ServerFlags import chat.revolt.api.schemas.User import chat.revolt.api.schemas.has +import chat.revolt.api.settings.GeoStateProvider import chat.revolt.api.settings.NotificationSettingsProvider import chat.revolt.api.settings.SyncedSettings import chat.revolt.composables.generic.GroupIcon @@ -914,7 +915,17 @@ fun ChannelItem( .fillMaxWidth()) { when (iconType) { is ChannelItemIconType.Channel -> { - ChannelIcon(iconType.type) + when { + GeoStateProvider.geoState?.isAgeRestrictedGeo == true && + channel.nsfw == true -> { + Icon( + painter = painterResource(R.drawable.icn_grid_3x3_off_24dp), + contentDescription = stringResource(R.string.geogate_channel_icon_alt), + ) + } + + else -> ChannelIcon(iconType.type) + } } is ChannelItemIconType.Painter -> { diff --git a/app/src/main/java/chat/revolt/composables/vectorassets/GeoGateUX.kt b/app/src/main/java/chat/revolt/composables/vectorassets/GeoGateUX.kt new file mode 100644 index 00000000..d5a04e9a --- /dev/null +++ b/app/src/main/java/chat/revolt/composables/vectorassets/GeoGateUX.kt @@ -0,0 +1,92 @@ +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 GeoGateUX: ImageVector + @Composable + get() { + if (_GeoGateUX != null) { + return _GeoGateUX!! + } + _GeoGateUX = ImageVector.Builder( + name = "GeoGate", + defaultWidth = 282.dp, + defaultHeight = 342.dp, + viewportWidth = 282f, + viewportHeight = 342f + ).apply { + path(fill = SolidColor(MaterialTheme.colorScheme.onBackground)) { + moveTo(173.19f, 30.41f) + lineTo(207.56f, 26.67f) + lineTo(214.24f, 88.06f) + lineTo(171.27f, 92.73f) + lineTo(167.48f, 80.72f) + lineTo(133.1f, 84.46f) + lineTo(136.83f, 118.76f) + curveTo(148.6f, 118.88f, 160.25f, 121.25f, 171.13f, 125.76f) + curveTo(182.29f, 130.38f, 192.44f, 137.16f, 200.98f, 145.71f) + curveTo(209.53f, 154.25f, 216.31f, 164.4f, 220.93f, 175.56f) + curveTo(225.56f, 186.73f, 227.94f, 198.69f, 227.94f, 210.78f) + curveTo(227.94f, 235.19f, 218.24f, 258.59f, 200.98f, 275.85f) + curveTo(183.72f, 293.11f, 160.32f, 302.8f, 135.91f, 302.8f) + curveTo(123.83f, 302.8f, 111.86f, 300.42f, 100.69f, 295.8f) + curveTo(89.53f, 291.17f, 79.38f, 284.4f, 70.84f, 275.85f) + curveTo(53.58f, 258.59f, 43.88f, 235.19f, 43.88f, 210.78f) + curveTo(43.88f, 186.37f, 53.58f, 162.96f, 70.84f, 145.71f) + curveTo(79.38f, 137.16f, 89.53f, 130.38f, 100.69f, 125.76f) + curveTo(108.32f, 122.6f, 116.32f, 120.49f, 124.48f, 119.47f) + lineTo(114.15f, 24.41f) + lineTo(169.4f, 18.4f) + lineTo(173.19f, 30.41f) + close() + moveTo(64.22f, 194.31f) + curveTo(63.03f, 199.55f, 62.29f, 205.07f, 62.29f, 210.78f) + curveTo(62.29f, 248.03f, 89.92f, 278.76f, 125.86f, 283.64f) + lineTo(126.71f, 265.99f) + curveTo(121.83f, 265.99f, 117.14f, 264.05f, 113.69f, 260.6f) + curveTo(110.24f, 257.15f, 108.3f, 252.47f, 108.3f, 247.59f) + verticalLineTo(238.39f) + lineTo(64.22f, 194.31f) + close() + moveTo(163.52f, 146.36f) + curveTo(163.52f, 151.24f, 161.58f, 155.92f, 158.13f, 159.38f) + curveTo(154.68f, 162.83f, 149.99f, 164.77f, 145.11f, 164.77f) + horizontalLineTo(126.71f) + verticalLineTo(183.17f) + curveTo(126.71f, 185.61f, 125.74f, 187.95f, 124.01f, 189.68f) + curveTo(122.29f, 191.4f, 119.95f, 192.37f, 117.51f, 192.37f) + horizontalLineTo(99.1f) + verticalLineTo(210.78f) + horizontalLineTo(154.32f) + curveTo(156.76f, 210.78f, 159.1f, 211.75f, 160.82f, 213.48f) + curveTo(162.55f, 215.2f, 163.52f, 217.54f, 163.52f, 219.98f) + verticalLineTo(247.59f) + horizontalLineTo(172.72f) + curveTo(180.91f, 247.59f, 187.81f, 253.02f, 190.21f, 260.38f) + curveTo(198.23f, 251.63f, 204.01f, 241.07f, 207.06f, 229.59f) + curveTo(210.1f, 218.12f, 210.33f, 206.07f, 207.7f, 194.5f) + curveTo(205.08f, 182.92f, 199.69f, 172.15f, 191.99f, 163.11f) + curveTo(184.3f, 154.07f, 174.53f, 147.03f, 163.52f, 142.59f) + verticalLineTo(146.36f) + close() + } + path(fill = SolidColor(MaterialTheme.colorScheme.error)) { + moveTo(281.1f, 314.38f) + lineTo(246.16f, 341.74f) + lineTo(0.5f, 28.08f) + lineTo(35.43f, 0.72f) + lineTo(281.1f, 314.38f) + close() + } + }.build() + + return _GeoGateUX!! + } + +@Suppress("ObjectPropertyName") +private var _GeoGateUX: ImageVector? = null diff --git a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt index 72633ae1..5c8b80ab 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt @@ -52,9 +52,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Menu import androidx.compose.material3.AssistChip import androidx.compose.material3.Button import androidx.compose.material3.Card @@ -646,622 +643,635 @@ fun ChannelScreen( } } ) { pv -> - Crossfade( - targetState = viewModel.ageGateUnlocked, - label = "ageGateUnlocked" - ) { ageGateUnlocked -> - if (ageGateUnlocked == false) { - ChannelScreenAgeGate( - onAccept = { - scope.launch { - viewModel.unlockAgeGate() - } - }, - onDeny = { - onToggleDrawer() - } - ) - } else if (ageGateUnlocked == null) { - Box(Modifier.fillMaxSize()) { - CircularProgressIndicator(modifier = Modifier.size(48.dp)) - } - } else if (ageGateUnlocked == true) { - Column( - modifier = Modifier - .padding(pv) - ) { - Box( - modifier = Modifier.weight(1f), - contentAlignment = Alignment.BottomCenter - ) { - LazyColumn( - state = lazyListState, - userScrollEnabled = !disableScroll, - reverseLayout = true, - contentPadding = PaddingValues(top = 16.dp, bottom = 32.dp) - ) { - - // If we don't have a guaranteed first item, the message list will not scroll - // to the bottom when new messages are added. Evil hack to make our other evil - // hack (clear/addAll) work. Too bad! - item(key = "guaranteed_first") { - Box {} + if (viewModel.showGeoGate) { + ChannelScreenGeoGate { onToggleDrawer() } + } else { + Crossfade( + targetState = viewModel.ageGateUnlocked, + label = "ageGateUnlocked" + ) { ageGateUnlocked -> + if (ageGateUnlocked == false) { + ChannelScreenAgeGate( + onAccept = { + scope.launch { + viewModel.unlockAgeGate() } + }, + onDeny = { + onToggleDrawer() + } + ) + } else if (ageGateUnlocked == null) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + ) { + CircularProgressIndicator(modifier = Modifier.size(48.dp)) + } + } else if (ageGateUnlocked == true) { + Column( + modifier = Modifier + .padding(pv) + ) { + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.BottomCenter + ) { + LazyColumn( + state = lazyListState, + userScrollEnabled = !disableScroll, + reverseLayout = true, + contentPadding = PaddingValues(top = 16.dp, bottom = 32.dp) + ) { - items( - viewModel.items.size, - key = { index -> - when (val item = viewModel.items[index]) { - is ChannelScreenItem.RegularMessage -> item.message.id!! - is ChannelScreenItem.ProspectiveMessage -> item.message.id!! - is ChannelScreenItem.FailedMessage -> item.message.id!! - is ChannelScreenItem.SystemMessage -> item.message.id!! - is ChannelScreenItem.DateDivider -> item.instant.toEpochMilliseconds() - is ChannelScreenItem.LoadTrigger -> index - is ChannelScreenItem.Loading -> index - } - }, - contentType = { index -> - when (viewModel.items.getOrNull(index)) { - null -> null - is ChannelScreenItem.RegularMessage -> "RegularMessage" - is ChannelScreenItem.ProspectiveMessage -> "ProspectiveMessage" - is ChannelScreenItem.FailedMessage -> "FailedMessage" - is ChannelScreenItem.SystemMessage -> "SystemMessage" - is ChannelScreenItem.DateDivider -> "DateDivider" - is ChannelScreenItem.LoadTrigger -> "LoadTrigger" - is ChannelScreenItem.Loading -> "Loading" - } + // If we don't have a guaranteed first item, the message list will not scroll + // to the bottom when new messages are added. Evil hack to make our other evil + // hack (clear/addAll) work. Too bad! + item(key = "guaranteed_first") { + Box {} } - ) { index -> - when (val item = viewModel.items[index]) { - is ChannelScreenItem.RegularMessage -> { - RegularMessage( - item.message, - viewModel.channel, - drawerIsOpen = drawerIsOpen, - setDrawerGestureEnabled = { - setDrawerGestureEnabled(it) - }, - setDisableScroll = { - disableScroll = it - }, - showMessageBottomSheet = { - messageContextSheetTarget = it - messageContextSheetShown = true - }, - showReactBottomSheet = { - item.message.id?.let { - reactSheetTarget = it - reactSheetShown = true - } - }, - putTextAtCursorPosition = viewModel::putAtCursorPosition, - replyToMessage = viewModel::addReplyTo, - scope = scope - ) - } - is ChannelScreenItem.ProspectiveMessage -> { - Box(Modifier.alpha(0.5f)) { - Message( - message = item.message, - onMessageContextMenu = { - // TODO Context menu that allows you to cancel send - }, - onAvatarClick = {}, - onNameClick = {}, - canReply = false, - onReply = {}, - onAddReaction = {} - ) + items( + viewModel.items.size, + key = { index -> + when (val item = viewModel.items[index]) { + is ChannelScreenItem.RegularMessage -> item.message.id!! + is ChannelScreenItem.ProspectiveMessage -> item.message.id!! + is ChannelScreenItem.FailedMessage -> item.message.id!! + is ChannelScreenItem.SystemMessage -> item.message.id!! + is ChannelScreenItem.DateDivider -> item.instant.toEpochMilliseconds() + is ChannelScreenItem.LoadTrigger -> index + is ChannelScreenItem.Loading -> index + } + }, + contentType = { index -> + when (viewModel.items.getOrNull(index)) { + null -> null + is ChannelScreenItem.RegularMessage -> "RegularMessage" + is ChannelScreenItem.ProspectiveMessage -> "ProspectiveMessage" + is ChannelScreenItem.FailedMessage -> "FailedMessage" + is ChannelScreenItem.SystemMessage -> "SystemMessage" + is ChannelScreenItem.DateDivider -> "DateDivider" + is ChannelScreenItem.LoadTrigger -> "LoadTrigger" + is ChannelScreenItem.Loading -> "Loading" } } + ) { index -> + when (val item = viewModel.items[index]) { + is ChannelScreenItem.RegularMessage -> { + RegularMessage( + item.message, + viewModel.channel, + drawerIsOpen = drawerIsOpen, + setDrawerGestureEnabled = { + setDrawerGestureEnabled(it) + }, + setDisableScroll = { + disableScroll = it + }, + showMessageBottomSheet = { + messageContextSheetTarget = it + messageContextSheetShown = true + }, + showReactBottomSheet = { + item.message.id?.let { + reactSheetTarget = it + reactSheetShown = true + } + }, + putTextAtCursorPosition = viewModel::putAtCursorPosition, + replyToMessage = viewModel::addReplyTo, + scope = scope + ) + } - is ChannelScreenItem.FailedMessage -> { - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) { - Column { + is ChannelScreenItem.ProspectiveMessage -> { + Box(Modifier.alpha(0.5f)) { Message( message = item.message, - onMessageContextMenu = {}, + onMessageContextMenu = { + // TODO Context menu that allows you to cancel send + }, onAvatarClick = {}, onNameClick = {}, canReply = false, onReply = {}, onAddReaction = {} ) - Row { - UserAvatarWidthPlaceholder() - Text( - stringResource(R.string.message_failed_to_send), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error.copy( - alpha = 0.8f - ), - modifier = Modifier.padding( - top = 4.dp, - bottom = 4.dp, - start = 20.dp - ) - ) - } } } - } - is ChannelScreenItem.SystemMessage -> { - SystemMessage(message = item.message) - } - - is ChannelScreenItem.DateDivider -> { - DateDivider(instant = item.instant) - } - - is ChannelScreenItem.LoadTrigger -> { - LaunchedEffect(Unit) { - Log.d( - "ChannelScreen", - "LoadTrigger: After ${item.after} Before ${item.before}" - ) - } - } - - is ChannelScreenItem.Loading -> { - Column( - modifier = Modifier - .fillMaxWidth() - .shimmer(rememberShimmer(ShimmerBounds.Window)), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - MessageSkeleton(MessageSkeletonVariant.One) - MessageSkeleton(MessageSkeletonVariant.Two) - MessageSkeleton(MessageSkeletonVariant.Three) - } - } - } - } - } - - TypingIndicator( - users = viewModel.typingUsers, - serverId = viewModel.channel?.server - ) - - androidx.compose.animation.AnimatedVisibility( - !isScrolledToBottom.value, - enter = slideInVertically( - animationSpec = RevoltTweenInt, - initialOffsetY = { it } - ) + fadeIn(animationSpec = RevoltTweenFloat), - exit = slideOutVertically( - animationSpec = RevoltTweenInt, - targetOffsetY = { it } - ) + fadeOut(animationSpec = RevoltTweenFloat) - ) { - SmallFloatingActionButton( - modifier = Modifier - .padding(bottom = scrollDownFABPadding) - .align(Alignment.BottomCenter) - .padding(16.dp), - onClick = { - scope.launch { - lazyListState.animateScrollToItem(0) - } - }, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant, - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) { - Icon( - painter = painterResource(R.drawable.icn_south_24dp), - contentDescription = stringResource(R.string.scroll_to_bottom) - ) - } - } - - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .align(Alignment.TopCenter) - .padding(8.dp) - ) { - if (viewModel.showPhysicalKeyboardSpark) { - Card { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.padding(16.dp) - ) { - Text( - stringResource(R.string.spark_keyboard_shortcuts), - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.SemiBold - ) - Text( - buildAnnotatedString { - val raw = - stringResource(R.string.spark_keyboard_shortcuts_description) - val before = raw.substringBefore("%1\$s") - val after = raw.substringAfter("%1\$s") - - append(before) - appendInlineContent("metaKey", "Meta") - append(" + /") - append(after) - }, - inlineContent = mapOf( - "metaKey" to InlineTextContent( - placeholder = Placeholder( - width = 1.em, - height = 1.em, - placeholderVerticalAlign = PlaceholderVerticalAlign.Center + is ChannelScreenItem.FailedMessage -> { + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) { + Column { + Message( + message = item.message, + onMessageContextMenu = {}, + onAvatarClick = {}, + onNameClick = {}, + canReply = false, + onReply = {}, + onAddReaction = {} ) - ) { - with(LocalDensity.current) { - Image( - painterResource(R.drawable.ic_meta_key_24dp), - contentDescription = null, - colorFilter = ColorFilter.tint( - LocalContentColor.current + Row { + UserAvatarWidthPlaceholder() + Text( + stringResource(R.string.message_failed_to_send), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error.copy( + alpha = 0.8f + ), + modifier = Modifier.padding( + top = 4.dp, + bottom = 4.dp, + start = 20.dp ) ) } } - ), - style = MaterialTheme.typography.bodyLarge - ) - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Button( - onClick = { - viewModel.dismissPhysicalKeyboardSpark() - }, - modifier = Modifier.weight(1f) - ) { - Text(stringResource(R.string.spark_keyboard_shortcuts_dismiss)) } - TextButton( - onClick = { - (context as Activity).requestShowKeyboardShortcuts() - }, - modifier = Modifier.weight(1f) + } + + is ChannelScreenItem.SystemMessage -> { + SystemMessage(message = item.message) + } + + is ChannelScreenItem.DateDivider -> { + DateDivider(instant = item.instant) + } + + is ChannelScreenItem.LoadTrigger -> { + LaunchedEffect(Unit) { + Log.d( + "ChannelScreen", + "LoadTrigger: After ${item.after} Before ${item.before}" + ) + } + } + + is ChannelScreenItem.Loading -> { + Column( + modifier = Modifier + .fillMaxWidth() + .shimmer(rememberShimmer(ShimmerBounds.Window)), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Text(stringResource(R.string.spark_keyboard_shortcuts_cta)) + MessageSkeleton(MessageSkeletonVariant.One) + MessageSkeleton(MessageSkeletonVariant.Two) + MessageSkeleton(MessageSkeletonVariant.Three) } } } } } - if (viewModel.channel?.channelType == ChannelType.VoiceChannel - && FeatureFlags.voiceChannels2_0Granted + TypingIndicator( + users = viewModel.typingUsers, + serverId = viewModel.channel?.server + ) + + androidx.compose.animation.AnimatedVisibility( + !isScrolledToBottom.value, + enter = slideInVertically( + animationSpec = RevoltTweenInt, + initialOffsetY = { it } + ) + fadeIn(animationSpec = RevoltTweenFloat), + exit = slideOutVertically( + animationSpec = RevoltTweenInt, + targetOffsetY = { it } + ) + fadeOut(animationSpec = RevoltTweenFloat) ) { - JoinVoiceChannelButton(channelId) + SmallFloatingActionButton( + modifier = Modifier + .padding(bottom = scrollDownFABPadding) + .align(Alignment.BottomCenter) + .padding(16.dp), + onClick = { + scope.launch { + lazyListState.animateScrollToItem(0) + } + }, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) { + Icon( + painter = painterResource(R.drawable.icn_south_24dp), + contentDescription = stringResource(R.string.scroll_to_bottom) + ) + } } - } - } - Column( - modifier = Modifier - .background(MaterialTheme.colorScheme.surfaceContainer) - .fillMaxWidth(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - AnimatedContent( - targetState = viewModel.denyMessageField, - label = "denyMessageField" - ) { deny -> - if (!deny) { - Column { - AnimatedVisibility( - visible = viewModel.draftReplyTo.isNotEmpty() && !viewModel.denyMessageField - ) { - ReplyManager( - replies = viewModel.draftReplyTo, - onToggleMention = { - scope.launch { viewModel.toggleMentionOnReply(it.id) } - }, - onRemove = { - viewModel.draftReplyTo.remove(it) - } - ) - } + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .align(Alignment.TopCenter) + .padding(8.dp) + ) { + if (viewModel.showPhysicalKeyboardSpark) { + Card { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(16.dp) + ) { + Text( + stringResource(R.string.spark_keyboard_shortcuts), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.SemiBold + ) + Text( + buildAnnotatedString { + val raw = + stringResource(R.string.spark_keyboard_shortcuts_description) + val before = raw.substringBefore("%1\$s") + val after = raw.substringAfter("%1\$s") - AnimatedVisibility( - visible = viewModel.draftAttachments.isNotEmpty() && !viewModel.denyMessageField - ) { - AttachmentManager( - attachments = viewModel.draftAttachments, - uploading = viewModel.attachmentUploadProgress > 0, - uploadProgress = viewModel.attachmentUploadProgress, - canRemove = true, - canPreview = true, - onRemove = { - viewModel.draftAttachments.remove(it) - }, - onToggleSpoiler = { - val index = viewModel.draftAttachments - .indexOfFirst { a -> a.pickerIdentifier == it.pickerIdentifier } - - if (index != -1) { - val attachment = - viewModel.draftAttachments[index] - viewModel.draftAttachments[index] = - attachment.copy( - spoiler = !attachment.spoiler + append(before) + appendInlineContent("metaKey", "Meta") + append(" + /") + append(after) + }, + inlineContent = mapOf( + "metaKey" to InlineTextContent( + placeholder = Placeholder( + width = 1.em, + height = 1.em, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center ) + ) { + with(LocalDensity.current) { + Image( + painterResource(R.drawable.ic_meta_key_24dp), + contentDescription = null, + colorFilter = ColorFilter.tint( + LocalContentColor.current + ) + ) + } + } + ), + style = MaterialTheme.typography.bodyLarge + ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Button( + onClick = { + viewModel.dismissPhysicalKeyboardSpark() + }, + modifier = Modifier.weight(1f) + ) { + Text(stringResource(R.string.spark_keyboard_shortcuts_dismiss)) + } + TextButton( + onClick = { + (context as Activity).requestShowKeyboardShortcuts() + }, + modifier = Modifier.weight(1f) + ) { + Text(stringResource(R.string.spark_keyboard_shortcuts_cta)) } } - ) + } } + } - AnimatedVisibility(visible = viewModel.editingMessage != null) { - Row(Modifier.padding(start = 24.dp, top = 8.dp)) { - AssistChip( - onClick = { - viewModel.editingMessage = null - viewModel.putDraftContent("", true) + if (viewModel.channel?.channelType == ChannelType.VoiceChannel + && FeatureFlags.voiceChannels2_0Granted + ) { + JoinVoiceChannelButton(channelId) + } + } + } + + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.surfaceContainer) + .fillMaxWidth(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + AnimatedContent( + targetState = viewModel.denyMessageField, + label = "denyMessageField" + ) { deny -> + if (!deny) { + Column { + AnimatedVisibility( + visible = viewModel.draftReplyTo.isNotEmpty() && !viewModel.denyMessageField + ) { + ReplyManager( + replies = viewModel.draftReplyTo, + onToggleMention = { + scope.launch { viewModel.toggleMentionOnReply(it.id) } }, - label = { - Text(stringResource(R.string.message_field_editing_message)) + onRemove = { + viewModel.draftReplyTo.remove(it) + } + ) + } + + AnimatedVisibility( + visible = viewModel.draftAttachments.isNotEmpty() && !viewModel.denyMessageField + ) { + AttachmentManager( + attachments = viewModel.draftAttachments, + uploading = viewModel.attachmentUploadProgress > 0, + uploadProgress = viewModel.attachmentUploadProgress, + canRemove = true, + canPreview = true, + onRemove = { + viewModel.draftAttachments.remove(it) }, + onToggleSpoiler = { + val index = viewModel.draftAttachments + .indexOfFirst { a -> a.pickerIdentifier == it.pickerIdentifier } + + if (index != -1) { + val attachment = + viewModel.draftAttachments[index] + viewModel.draftAttachments[index] = + attachment.copy( + spoiler = !attachment.spoiler + ) + } + } + ) + } + + AnimatedVisibility(visible = viewModel.editingMessage != null) { + Row(Modifier.padding(start = 24.dp, top = 8.dp)) { + AssistChip( + onClick = { + viewModel.editingMessage = null + viewModel.putDraftContent("", true) + }, + label = { + Text(stringResource(R.string.message_field_editing_message)) + }, + leadingIcon = { + Icon( + painter = painterResource(R.drawable.icn_edit_24dp), + contentDescription = null + ) + }, + trailingIcon = { + Icon( + painter = painterResource(R.drawable.icn_close_24dp), + contentDescription = stringResource(R.string.message_field_editing_message_cancel_alt), + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.alpha(0.8f) + ) + } + ) + } + } + + MessageField( + initialValue = viewModel.initialTextFieldValue, + initialValueDirtyMarker = viewModel.initialTextFieldValueDirtyMarker, + onValueChange = viewModel::putDraftContent, + onAddAttachment = { + if (viewModel.activePane == ChannelScreenActivePane.AttachmentPicker) { + viewModel.activePane = + ChannelScreenActivePane.None + } else { + viewModel.activePane = + ChannelScreenActivePane.AttachmentPicker + } + }, + onCommitAttachment = { + processFileUri(it, null) + }, + onPickEmoji = { + if (viewModel.activePane == ChannelScreenActivePane.EmojiPicker) { + viewModel.activePane = + ChannelScreenActivePane.None + } else { + viewModel.activePane = + ChannelScreenActivePane.EmojiPicker + } + }, + onSendMessage = viewModel::sendPendingMessage, + channelType = viewModel.channel?.channelType + ?: ChannelType.TextChannel, + channelName = viewModel.channel?.let { channel -> + ChannelUtils.resolveName(channel) + } + ?: stringResource(R.string.unknown), + onFocusChange = { isFocused -> + if (isFocused && viewModel.activePane != ChannelScreenActivePane.None) { + viewModel.activePane = + ChannelScreenActivePane.None + imeInTransition = true + } + }, + forceSendButton = viewModel.draftAttachments.isNotEmpty(), + canAttach = (channelPermissions has PermissionBit.UploadFiles) && viewModel.editingMessage == null, + serverId = viewModel.channel?.server, + channelId = channelId, + failedValidation = viewModel.draftContent.length > 2000, + valueIsBlank = viewModel.draftContent.isBlank(), + cancelEdit = { + viewModel.editingMessage = null + viewModel.putDraftContent("", true) + } + ) + + DropdownMenu( + expanded = viewModel.activePane == ChannelScreenActivePane.AttachmentPicker && notEnoughSpaceForPanes, + onDismissRequest = { + viewModel.activePane = ChannelScreenActivePane.None + } + ) { + DropdownMenuItem( leadingIcon = { Icon( - painter = painterResource(R.drawable.icn_edit_24dp), - contentDescription = null + painter = painterResource(R.drawable.icn_attach_file_24dp), + contentDescription = null // Provided by text below ) }, - trailingIcon = { + text = { Text(stringResource(R.string.file_picker_chip_documents)) }, + onClick = { + openDocumentPickerCallback() + viewModel.activePane = + ChannelScreenActivePane.None + } + ) + DropdownMenuItem( + leadingIcon = { Icon( - painter = painterResource(R.drawable.icn_close_24dp), - contentDescription = stringResource(R.string.message_field_editing_message_cancel_alt), - tint = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.alpha(0.8f) + painter = painterResource(R.drawable.icn_camera_24dp), + contentDescription = null // Provided by text below ) + }, + text = { Text(stringResource(R.string.file_picker_chip_camera)) }, + onClick = { + openCameraCallback() + viewModel.activePane = + ChannelScreenActivePane.None + } + ) + DropdownMenuItem( + leadingIcon = { + Icon( + painter = painterResource(R.drawable.icn_photo_library_24dp), + contentDescription = null // Provided by text below + ) + }, + text = { Text(stringResource(R.string.file_picker_chip_photo_picker)) }, + onClick = { + openPhotoPickerCallback() + viewModel.activePane = + ChannelScreenActivePane.None } ) } } - - MessageField( - initialValue = viewModel.initialTextFieldValue, - initialValueDirtyMarker = viewModel.initialTextFieldValueDirtyMarker, - onValueChange = viewModel::putDraftContent, - onAddAttachment = { - if (viewModel.activePane == ChannelScreenActivePane.AttachmentPicker) { - viewModel.activePane = ChannelScreenActivePane.None - } else { - viewModel.activePane = - ChannelScreenActivePane.AttachmentPicker - } - }, - onCommitAttachment = { - processFileUri(it, null) - }, - onPickEmoji = { - if (viewModel.activePane == ChannelScreenActivePane.EmojiPicker) { - viewModel.activePane = ChannelScreenActivePane.None - } else { - viewModel.activePane = - ChannelScreenActivePane.EmojiPicker - } - }, - onSendMessage = viewModel::sendPendingMessage, - channelType = viewModel.channel?.channelType - ?: ChannelType.TextChannel, - channelName = viewModel.channel?.let { channel -> - ChannelUtils.resolveName(channel) - } - ?: stringResource(R.string.unknown), - onFocusChange = { isFocused -> - if (isFocused && viewModel.activePane != ChannelScreenActivePane.None) { - viewModel.activePane = ChannelScreenActivePane.None - imeInTransition = true - } - }, - forceSendButton = viewModel.draftAttachments.isNotEmpty(), - canAttach = (channelPermissions has PermissionBit.UploadFiles) && viewModel.editingMessage == null, - serverId = viewModel.channel?.server, - channelId = channelId, - failedValidation = viewModel.draftContent.length > 2000, - valueIsBlank = viewModel.draftContent.isBlank(), - cancelEdit = { - viewModel.editingMessage = null - viewModel.putDraftContent("", true) - } - ) - - DropdownMenu( - expanded = viewModel.activePane == ChannelScreenActivePane.AttachmentPicker && notEnoughSpaceForPanes, - onDismissRequest = { - viewModel.activePane = ChannelScreenActivePane.None - } + } else { + Box( + modifier = Modifier + .padding(horizontal = 32.dp, vertical = 16.dp) ) { - DropdownMenuItem( - leadingIcon = { - Icon( - painter = painterResource(R.drawable.icn_attach_file_24dp), - contentDescription = null // Provided by text below - ) - }, - text = { Text(stringResource(R.string.file_picker_chip_documents)) }, - onClick = { - openDocumentPickerCallback() - viewModel.activePane = ChannelScreenActivePane.None - } - ) - DropdownMenuItem( - leadingIcon = { - Icon( - painter = painterResource(R.drawable.icn_camera_24dp), - contentDescription = null // Provided by text below - ) - }, - text = { Text(stringResource(R.string.file_picker_chip_camera)) }, - onClick = { - openCameraCallback() - viewModel.activePane = ChannelScreenActivePane.None - } - ) - DropdownMenuItem( - leadingIcon = { - Icon( - painter = painterResource(R.drawable.icn_photo_library_24dp), - contentDescription = null // Provided by text below - ) - }, - text = { Text(stringResource(R.string.file_picker_chip_photo_picker)) }, - onClick = { - openPhotoPickerCallback() - viewModel.activePane = ChannelScreenActivePane.None - } + Text( + stringResource(viewModel.denyMessageFieldReasonResource), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center ) } } - } else { - Box( - modifier = Modifier - .padding(horizontal = 32.dp, vertical = 16.dp) - ) { - Text( - stringResource(viewModel.denyMessageFieldReasonResource), - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center - ) - } } - } - if (viewModel.activePane == ChannelScreenActivePane.None && !imeInTransition) { - Spacer( - Modifier - .imePadding() - .navigationBarsPadding() - .background(MaterialTheme.colorScheme.surfaceContainer) - ) - } else { - if (!notEnoughSpaceForPanes) { - Box( - Modifier - .heightIn(min = pxAsDp(fallbackKeyboardHeight)) - ) { - Box( - Modifier.then( - if (emojiSearchFocused) { - Modifier.requiredHeight( - pxAsDp( - max( - imeCurrentInset * 2, - fallbackKeyboardHeight - ) - ) - ) - } else { - Modifier.requiredHeight( - pxAsDp( - fallbackKeyboardHeight - ) - ) - } - ) - ) { - when (viewModel.activePane) { - ChannelScreenActivePane.EmojiPicker -> { - BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.EmojiPicker) { - viewModel.activePane = - ChannelScreenActivePane.None - } - - Column( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surfaceContainer) - .padding(4.dp) - .navigationBarsPadding() - ) { - EmojiPicker( - onEmojiSelected = viewModel::putAtCursorPosition, - bottomInset = pxAsDp( - max( - imeCurrentInset - navigationBarsInset, - 0 - ) - ), - onSearchFocus = { - emojiSearchFocused = it - } - ) - } - } - - ChannelScreenActivePane.AttachmentPicker -> { - BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.AttachmentPicker) { - viewModel.activePane = - ChannelScreenActivePane.None - } - - MediaPickerGateway( - onOpenPhotoPicker = { - openPhotoPickerCallback() - viewModel.activePane = - ChannelScreenActivePane.None - }, - onOpenDocumentPicker = { - openDocumentPickerCallback() - viewModel.activePane = - ChannelScreenActivePane.None - }, - onOpenCamera = { - openCameraCallback() - viewModel.activePane = - ChannelScreenActivePane.None - }, - ) - } - - else -> { - // Do nothing - } - } - } - Box(Modifier.imePadding()) - } - } else { - if (viewModel.activePane == ChannelScreenActivePane.EmojiPicker) { - BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.EmojiPicker) { - viewModel.activePane = - ChannelScreenActivePane.None - } - - Column( - modifier = Modifier - .fillMaxWidth() - .height(600.dp) - .background(MaterialTheme.colorScheme.surfaceContainer) - .padding(4.dp) - .navigationBarsPadding() - ) { - EmojiPicker( - onEmojiSelected = viewModel::putAtCursorPosition, - bottomInset = pxAsDp( - max( - imeCurrentInset - navigationBarsInset, - 0 - ) - ), - onSearchFocus = { - emojiSearchFocused = it - } - ) - } - } - Box( + if (viewModel.activePane == ChannelScreenActivePane.None && !imeInTransition) { + Spacer( Modifier .imePadding() .navigationBarsPadding() + .background(MaterialTheme.colorScheme.surfaceContainer) ) + } else { + if (!notEnoughSpaceForPanes) { + Box( + Modifier + .heightIn(min = pxAsDp(fallbackKeyboardHeight)) + ) { + Box( + Modifier.then( + if (emojiSearchFocused) { + Modifier.requiredHeight( + pxAsDp( + max( + imeCurrentInset * 2, + fallbackKeyboardHeight + ) + ) + ) + } else { + Modifier.requiredHeight( + pxAsDp( + fallbackKeyboardHeight + ) + ) + } + ) + ) { + when (viewModel.activePane) { + ChannelScreenActivePane.EmojiPicker -> { + BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.EmojiPicker) { + viewModel.activePane = + ChannelScreenActivePane.None + } + + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceContainer) + .padding(4.dp) + .navigationBarsPadding() + ) { + EmojiPicker( + onEmojiSelected = viewModel::putAtCursorPosition, + bottomInset = pxAsDp( + max( + imeCurrentInset - navigationBarsInset, + 0 + ) + ), + onSearchFocus = { + emojiSearchFocused = it + } + ) + } + } + + ChannelScreenActivePane.AttachmentPicker -> { + BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.AttachmentPicker) { + viewModel.activePane = + ChannelScreenActivePane.None + } + + MediaPickerGateway( + onOpenPhotoPicker = { + openPhotoPickerCallback() + viewModel.activePane = + ChannelScreenActivePane.None + }, + onOpenDocumentPicker = { + openDocumentPickerCallback() + viewModel.activePane = + ChannelScreenActivePane.None + }, + onOpenCamera = { + openCameraCallback() + viewModel.activePane = + ChannelScreenActivePane.None + }, + ) + } + + else -> { + // Do nothing + } + } + } + Box(Modifier.imePadding()) + } + } else { + if (viewModel.activePane == ChannelScreenActivePane.EmojiPicker) { + BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.EmojiPicker) { + viewModel.activePane = + ChannelScreenActivePane.None + } + + Column( + modifier = Modifier + .fillMaxWidth() + .height(600.dp) + .background(MaterialTheme.colorScheme.surfaceContainer) + .padding(4.dp) + .navigationBarsPadding() + ) { + EmojiPicker( + onEmojiSelected = viewModel::putAtCursorPosition, + bottomInset = pxAsDp( + max( + imeCurrentInset - navigationBarsInset, + 0 + ) + ), + onSearchFocus = { + emojiSearchFocused = it + } + ) + } + } + Box( + Modifier + .imePadding() + .navigationBarsPadding() + ) + } } } } diff --git a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenGeoGate.kt b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenGeoGate.kt new file mode 100644 index 00000000..9fa9d6b6 --- /dev/null +++ b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenGeoGate.kt @@ -0,0 +1,81 @@ +package chat.revolt.screens.chat.views.channel + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import chat.revolt.R +import chat.revolt.api.settings.GeoStateProvider +import chat.revolt.composables.vectorassets.GeoGateUX + +@Composable +fun ChannelScreenGeoGate( + onAcknowledge: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .imePadding(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically) + ) { + Image( + imageVector = GeoGateUX, + contentDescription = null, + modifier = Modifier.size(128.dp), + ) + + Text( + text = stringResource(R.string.geogate_header), + style = MaterialTheme.typography.titleMedium.copy( + textAlign = TextAlign.Center + ), + modifier = Modifier.padding(horizontal = 16.dp) + ) + + when (GeoStateProvider.geoState?.countryCode) { + "GB" -> { + Text( + text = stringResource(R.string.geogate_description_variant_osa_uk_25), + style = MaterialTheme.typography.bodyMedium.copy( + textAlign = TextAlign.Center + ), + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + + else -> { + Text( + text = stringResource(R.string.geogate_description), + style = MaterialTheme.typography.bodyMedium.copy( + textAlign = TextAlign.Center + ), + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + } + + Button(onClick = { onAcknowledge() }) { + Text(stringResource(R.string.geogate_acknowledge)) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun GeoGatePreview() { + ChannelScreenGeoGate { } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt index 3d9bc031..1402e278 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt @@ -43,6 +43,7 @@ import chat.revolt.api.routes.user.addUserIfUnknown import chat.revolt.api.routes.user.fetchUser import chat.revolt.api.schemas.Channel import chat.revolt.api.schemas.Message +import chat.revolt.api.settings.GeoStateProvider import chat.revolt.callbacks.Action import chat.revolt.callbacks.ActionChannel import chat.revolt.callbacks.UiCallback @@ -103,6 +104,7 @@ class ChannelScreenViewModel @Inject constructor( var editingMessage by mutableStateOf(null) var ageGateUnlocked by mutableStateOf(null) + var showGeoGate by mutableStateOf(false) init { viewModelScope.launch { @@ -126,6 +128,10 @@ class ChannelScreenViewModel @Inject constructor( this.denyMessageFieldReasonResource = R.string.typing_blank this.editingMessage = null this.ageGateUnlocked = channel?.nsfw != true + this.showGeoGate = when { + channel?.nsfw == true && GeoStateProvider.geoState?.isAgeRestrictedGeo == true -> true + else -> false + } viewModelScope.launch { if (ageGateUnlocked != true) { ageGateUnlocked = AgeGateUnlockedStorageProvider.getAgeGateUnlocked() diff --git a/app/src/main/res/drawable/icn_grid_3x3_24dp.xml b/app/src/main/res/drawable/icn_grid_3x3_24dp.xml new file mode 100644 index 00000000..507c5ea8 --- /dev/null +++ b/app/src/main/res/drawable/icn_grid_3x3_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/icn_grid_3x3_off_24dp.xml b/app/src/main/res/drawable/icn_grid_3x3_off_24dp.xml new file mode 100644 index 00000000..726b1c42 --- /dev/null +++ b/app/src/main/res/drawable/icn_grid_3x3_off_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a5b3bf3..7695422e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -794,4 +794,10 @@ Messaging New Line Send Message + + Not available in your region + Unavailable channel + Revolt may block content in certain jurisdictions in response to legislation or legal notices + This channel is not available in your region while we review options on legal compliance. + Acknowledge From 302ad04c69ec982815284a609f53433cbd784faf Mon Sep 17 00:00:00 2001 From: Infi Date: Sat, 26 Jul 2025 07:23:34 +0200 Subject: [PATCH 3/6] chore: bump to 1.3.6a Signed-off-by: Infi --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3c2afdd0..65194852 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -80,8 +80,8 @@ android { applicationId = "chat.revolt" minSdk = 24 targetSdk = 35 - versionCode = Integer.parseInt("001_003_006".replace("_", ""), 10) - versionName = "1.3.6" + versionCode = Integer.parseInt("001_003_106".replace("_", ""), 10) + versionName = "1.3.6a" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From f49b25b4e34231d64c2e316af00b026688dc3439 Mon Sep 17 00:00:00 2001 From: AbronStudio Date: Sat, 26 Jul 2025 12:17:57 +0330 Subject: [PATCH 4/6] feat(auth): implement platform selection screen with API URL input Add platform selection UI with PepChat and Revolt options Implement clickable platform cards that set the API URL Create custom styled text field for API URL input Add confirm button with localized string resource Fix vertical alignment of placeholder text in text field --- .../chooseplatform/ChoosePlatformScreen.kt | 218 +++++++++++++++++ .../choose_platform_hligh_hand_img.xml | 219 ++++++++++++++++++ .../main/res/drawable/ic_double_connect.xml | 9 + app/src/main/res/drawable/ic_revolt_r.xml | 15 ++ .../res/drawable/onboarding_fly_image.xml | 180 ++++++++++++++ app/src/main/res/drawable/pep_logo_wide.xml | 18 ++ 6 files changed, 659 insertions(+) create mode 100644 app/src/main/java/chat/revolt/screens/chooseplatform/ChoosePlatformScreen.kt create mode 100644 app/src/main/res/drawable/choose_platform_hligh_hand_img.xml create mode 100644 app/src/main/res/drawable/ic_double_connect.xml create mode 100644 app/src/main/res/drawable/ic_revolt_r.xml create mode 100644 app/src/main/res/drawable/onboarding_fly_image.xml create mode 100644 app/src/main/res/drawable/pep_logo_wide.xml diff --git a/app/src/main/java/chat/revolt/screens/chooseplatform/ChoosePlatformScreen.kt b/app/src/main/java/chat/revolt/screens/chooseplatform/ChoosePlatformScreen.kt new file mode 100644 index 00000000..ae77ecfd --- /dev/null +++ b/app/src/main/java/chat/revolt/screens/chooseplatform/ChoosePlatformScreen.kt @@ -0,0 +1,218 @@ +package chat.revolt.screens.chooseplatform + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import chat.revolt.R + +@Composable +fun ChoosePlatformScreen(navController: NavController) { + val scrollState = rememberScrollState() + val apiUrlValue = remember { + mutableStateOf("") + } + + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .safeDrawingPadding() + .fillMaxSize() + .verticalScroll(scrollState), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + modifier = Modifier.padding(vertical = 24.dp), + painter = painterResource(id = R.drawable.pep_logo_wide), + contentDescription = "Pep Wide Logo" + ) + + Image( + modifier = Modifier.padding(vertical = 24.dp), + painter = painterResource(id = R.drawable.choose_platform_hligh_hand_img), + contentDescription = "Choose Platform Highlight Image" + ) + + Column( + modifier = Modifier + .padding(vertical = 32.dp) + ) { + Text( + text = "Welcome to PepChat!", + style = MaterialTheme.typography.displaySmall.copy( + fontSize = 26.sp, + fontWeight = FontWeight.Black, + textAlign = TextAlign.Center + ), + modifier = Modifier + .padding(bottom = 4.dp) + .fillMaxWidth() + ) + + Text( + text = "Choose your preferred platform!", + color = MaterialTheme.colorScheme.onBackground.copy( + alpha = 0.5f + ), + style = MaterialTheme.typography.titleMedium.copy( + fontSize = 16.sp, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Normal + ), + modifier = Modifier + .padding(horizontal = 20.dp) + .fillMaxWidth() + ) + } + Row( + modifier = Modifier + .padding(bottom = 32.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Card( + modifier = Modifier + .padding(bottom = 8.dp) + .size(72.dp) + .clickable { + apiUrlValue.value = "https://api.revolt.chat" + }, + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(id = R.drawable.ic_launcher_foreground), + contentDescription = "Discord Logo" + ) + } + } + Text( + text = "PepChat", + color = MaterialTheme.colorScheme.onBackground.copy( + alpha = 0.5f + ), + style = MaterialTheme.typography.displaySmall.copy( + fontSize = 16.sp, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Normal + ), + ) + } + Spacer(modifier = Modifier.padding(horizontal = 24.dp)) + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Card( + modifier = Modifier + .padding(bottom = 8.dp) + .size(72.dp) + .clickable { + apiUrlValue.value = "https://api.revolt.chat" + }, + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(id = R.drawable.ic_revolt_r), + contentDescription = "Discord Logo" + ) + } + } + Text( + text = "Revolt", + color = MaterialTheme.colorScheme.onBackground.copy( + alpha = 0.5f + ), + style = MaterialTheme.typography.displaySmall.copy( + fontSize = 16.sp, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Normal + ), + ) + } + } + OutlinedTextField( + value = apiUrlValue.value, + onValueChange = { apiUrlValue.value = it }, + placeholder = { + Text( + text = "Enter custom API url...", + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + }, + leadingIcon = { + Icon( + painter = painterResource(R.drawable.ic_double_connect), + contentDescription = "connect icon", + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + }, + shape = MaterialTheme.shapes.medium, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + Button( + onClick = { navController.navigate("login/greeting") }, + modifier = Modifier + .padding(top = 32.dp, bottom = 16.dp) + .fillMaxWidth() + .testTag("confirm_platform_button") + ) { + Text(text = stringResource(R.string.confirm)) + } + Box( + modifier = Modifier + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text( + text = "PepChat v1.0", + color = MaterialTheme.colorScheme.onBackground.copy( + alpha = 0.5f + ), + style = MaterialTheme.typography.displaySmall.copy( + fontSize = 12.sp, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Normal + ), + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/choose_platform_hligh_hand_img.xml b/app/src/main/res/drawable/choose_platform_hligh_hand_img.xml new file mode 100644 index 00000000..169e07a1 --- /dev/null +++ b/app/src/main/res/drawable/choose_platform_hligh_hand_img.xml @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_double_connect.xml b/app/src/main/res/drawable/ic_double_connect.xml new file mode 100644 index 00000000..a910056b --- /dev/null +++ b/app/src/main/res/drawable/ic_double_connect.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_revolt_r.xml b/app/src/main/res/drawable/ic_revolt_r.xml new file mode 100644 index 00000000..502ad162 --- /dev/null +++ b/app/src/main/res/drawable/ic_revolt_r.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/app/src/main/res/drawable/onboarding_fly_image.xml b/app/src/main/res/drawable/onboarding_fly_image.xml new file mode 100644 index 00000000..2279cbb0 --- /dev/null +++ b/app/src/main/res/drawable/onboarding_fly_image.xml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/pep_logo_wide.xml b/app/src/main/res/drawable/pep_logo_wide.xml new file mode 100644 index 00000000..58e86020 --- /dev/null +++ b/app/src/main/res/drawable/pep_logo_wide.xml @@ -0,0 +1,18 @@ + + + + + From 80db00ee2a1bccbeeaf5bc03038d2a72325eb1ea Mon Sep 17 00:00:00 2001 From: AbronStudio Date: Sat, 26 Jul 2025 12:18:06 +0330 Subject: [PATCH 5/6] feat(auth): implement platform selection screen with API URL input Add platform selection UI with PepChat and Revolt options Implement clickable platform cards that set the API URL Create custom styled text field for API URL input Add confirm button with localized string resource Fix vertical alignment of placeholder text in text field --- .idea/codeStyles/Project.xml | 35 ++++++++++ .../chat/revolt/activities/MainActivity.kt | 4 +- .../composables/generic/FormTextField.kt | 8 ++- .../screens/login/LoginGreetingScreen.kt | 64 ++++--------------- .../res/drawable/ic_launcher_foreground.xml | 15 +++-- app/src/main/res/values/strings.xml | 1 + 6 files changed, 69 insertions(+), 58 deletions(-) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 7643783a..7f70f7ef 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,5 +1,40 @@ + + + diff --git a/app/src/main/java/chat/revolt/activities/MainActivity.kt b/app/src/main/java/chat/revolt/activities/MainActivity.kt index 153b10c6..64e057d4 100644 --- a/app/src/main/java/chat/revolt/activities/MainActivity.kt +++ b/app/src/main/java/chat/revolt/activities/MainActivity.kt @@ -96,6 +96,7 @@ import chat.revolt.screens.about.AboutScreen import chat.revolt.screens.about.AttributionScreen import chat.revolt.screens.chat.ChatRouterScreen import chat.revolt.screens.chat.views.channel.ChannelScreen +import chat.revolt.screens.chooseplatform.ChoosePlatformScreen import chat.revolt.screens.create.CreateGroupScreen import chat.revolt.screens.labs.LabsRootScreen import chat.revolt.screens.login.LoginGreetingScreen @@ -200,7 +201,7 @@ class MainActivityViewModel @Inject constructor( Log.d("MainActivity", "We can reach Revolt, checking if we're logged in") val token = kvStorage.get("sessionToken") - ?: return@launch startWithDestination("login/greeting") + ?: return@launch startWithDestination("choose-platform") val id = kvStorage.get("sessionId") ?: "" Log.d( @@ -563,6 +564,7 @@ fun AppEntrypoint( ) } + composable("choose-platform") { ChoosePlatformScreen(navController) } composable("login/greeting") { LoginGreetingScreen(navController) } composable("login/login") { LoginScreen(navController) } composable("login/mfa/{mfaTicket}/{allowedAuthTypes}") { backStackEntry -> diff --git a/app/src/main/java/chat/revolt/composables/generic/FormTextField.kt b/app/src/main/java/chat/revolt/composables/generic/FormTextField.kt index 19d1f520..fe4eeeea 100644 --- a/app/src/main/java/chat/revolt/composables/generic/FormTextField.kt +++ b/app/src/main/java/chat/revolt/composables/generic/FormTextField.kt @@ -22,6 +22,9 @@ fun FormTextField( type: KeyboardType = KeyboardType.Text, action: ImeAction = ImeAction.Done, supportingText: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null, singleLine: Boolean = true, enabled: Boolean = true ) { @@ -29,11 +32,14 @@ fun FormTextField( value = value, onValueChange = onChange, singleLine = singleLine, + placeholder = placeholder, keyboardOptions = KeyboardOptions(keyboardType = type, imeAction = action), visualTransformation = if (type == KeyboardType.Password) PasswordVisualTransformation() else VisualTransformation.None, label = { Text(label) }, supportingText = supportingText, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, enabled = enabled, modifier = modifier ) -} +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/screens/login/LoginGreetingScreen.kt b/app/src/main/java/chat/revolt/screens/login/LoginGreetingScreen.kt index 40433867..0fc711fd 100644 --- a/app/src/main/java/chat/revolt/screens/login/LoginGreetingScreen.kt +++ b/app/src/main/java/chat/revolt/screens/login/LoginGreetingScreen.kt @@ -14,16 +14,13 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding -import androidx.compose.foundation.layout.width import androidx.compose.material3.Button import androidx.compose.material3.ElevatedButton import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -31,7 +28,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag @@ -42,12 +38,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController -import chat.revolt.BuildConfig import chat.revolt.R -import chat.revolt.api.REVOLT_MARKETING -import chat.revolt.composables.generic.AnyLink -import chat.revolt.composables.generic.Weblink -import com.chuckerteam.chucker.api.Chucker @OptIn(ExperimentalFoundationApi::class) @Composable @@ -59,7 +50,7 @@ fun LoginGreetingScreen(navController: NavController) { Column( modifier = Modifier .fillMaxSize() - .padding(vertical = 20.dp, horizontal = 0.dp) + .padding(vertical = 20.dp, horizontal = 16.dp) .safeDrawingPadding(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally @@ -71,13 +62,11 @@ fun LoginGreetingScreen(navController: NavController) { horizontalAlignment = Alignment.CenterHorizontally ) { Image( - painter = painterResource(id = R.drawable.revolt_logo_wide), - colorFilter = ColorFilter.tint(LocalContentColor.current), + painter = painterResource(id = R.drawable.onboarding_fly_image), contentDescription = "Revolt Logo", contentScale = ContentScale.Fit, modifier = Modifier - .height(55.dp) - .padding(bottom = 10.dp) + .padding(bottom = 16.dp) .combinedClickable( interactionSource = remember(::MutableInteractionSource), indication = null, @@ -100,7 +89,14 @@ fun LoginGreetingScreen(navController: NavController) { } ) ) + } + Column( + modifier = Modifier + .fillMaxWidth(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { Text( text = stringResource(R.string.login_onboarding_heading), style = MaterialTheme.typography.displaySmall.copy( @@ -109,7 +105,7 @@ fun LoginGreetingScreen(navController: NavController) { textAlign = TextAlign.Center ), modifier = Modifier - .padding(horizontal = 20.dp, vertical = 10.dp) + .padding(bottom = 4.dp) .fillMaxWidth() ) @@ -124,17 +120,12 @@ fun LoginGreetingScreen(navController: NavController) { fontWeight = FontWeight.Normal ), modifier = Modifier - .padding(horizontal = 20.dp, vertical = 10.dp) + .padding(horizontal = 20.dp) .fillMaxWidth() ) - } - Column( - modifier = Modifier - .width(200.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { + Spacer(modifier = Modifier.height(32.dp)) + Button( onClick = { navController.navigate("login/login") }, modifier = Modifier @@ -177,33 +168,6 @@ fun LoginGreetingScreen(navController: NavController) { } Spacer(modifier = Modifier.height(40.dp)) - - CompositionLocalProvider( - LocalTextStyle provides LocalTextStyle.current.copy(textAlign = TextAlign.Center) - ) { - Weblink( - text = stringResource(R.string.terms_of_service), - url = "$REVOLT_MARKETING/terms" - ) - Weblink( - text = stringResource(R.string.privacy_policy), - url = "$REVOLT_MARKETING/privacy" - ) - Weblink( - text = stringResource(R.string.community_guidelines), - url = "$REVOLT_MARKETING/aup" - ) - if (BuildConfig.DEBUG) { - AnyLink( - text = "Debug: Chucker", - action = { - Chucker.getLaunchIntent(context).apply { - context.startActivity(this) - } - } - ) - } - } } } } diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 6a29b828..e9c2f2af 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,9 +1,12 @@ + android:width="72dp" + android:height="72dp" + android:viewportWidth="72" + android:viewportHeight="72"> + android:pathData="M32.888,21.721V31.138H26.569V21.721H32.888ZM51.649,21.721V31.138H45.33V21.721H51.649Z" + android:fillColor="#662D91"/> + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a5b3bf3..bccf2571 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,6 +7,7 @@ Cancel Share Let\'s go + Confirm Fetching some info, hang in there… Hold your horses! You\'re doing that too often in a short amount of time. From 6a413f8e17f338aec0242672af6233d310ab6001 Mon Sep 17 00:00:00 2001 From: AbronStudio Date: Sat, 26 Jul 2025 13:39:10 +0330 Subject: [PATCH 6/6] feat(auth): update platform selection screen and login text - Add "Continue" string resource for navigation - Update login screen heading to "Welcome back!" - Add login subheading "Great to see you again!" - Update registration form heading to "Make it Official!" - Change registration data text to "New Here? Let's Get You Set Up!" - Change platform button text from "Login" to "Confirm" - Set API URL to "https://api.revolt.chat" for both platform options --- .../screens/login/LoginGreetingScreen.kt | 2 +- .../chat/revolt/screens/login/LoginScreen.kt | 155 ++++++++------ .../screens/register/RegisterDetailsScreen.kt | 152 ++++++------- .../res/drawable/login_charachter_img.xml | 201 ++++++++++++++++++ app/src/main/res/values/strings.xml | 8 +- 5 files changed, 372 insertions(+), 146 deletions(-) create mode 100644 app/src/main/res/drawable/login_charachter_img.xml diff --git a/app/src/main/java/chat/revolt/screens/login/LoginGreetingScreen.kt b/app/src/main/java/chat/revolt/screens/login/LoginGreetingScreen.kt index 0fc711fd..1ffa83c0 100644 --- a/app/src/main/java/chat/revolt/screens/login/LoginGreetingScreen.kt +++ b/app/src/main/java/chat/revolt/screens/login/LoginGreetingScreen.kt @@ -138,7 +138,7 @@ fun LoginGreetingScreen(navController: NavController) { Spacer(modifier = Modifier.height(5.dp)) ElevatedButton( - onClick = { navController.navigate("register/greeting") }, + onClick = { navController.navigate("register/details") }, modifier = Modifier .fillMaxWidth() .testTag("view_signup_page_button") diff --git a/app/src/main/java/chat/revolt/screens/login/LoginScreen.kt b/app/src/main/java/chat/revolt/screens/login/LoginScreen.kt index 9b5931f5..ebe969a6 100644 --- a/app/src/main/java/chat/revolt/screens/login/LoginScreen.kt +++ b/app/src/main/java/chat/revolt/screens/login/LoginScreen.kt @@ -1,6 +1,7 @@ package chat.revolt.screens.login import android.util.Log +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -10,18 +11,20 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.input.TextObfuscationMode import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.SecureTextField import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -31,7 +34,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.autofill.ContentType -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -150,6 +152,7 @@ class LoginViewModel @Inject constructor( } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun LoginScreen(navController: NavController, viewModel: LoginViewModel = hiltViewModel()) { val passwordTextFieldState = rememberTextFieldState() @@ -158,8 +161,6 @@ fun LoginScreen(navController: NavController, viewModel: LoginViewModel = hiltVi } val showPassword = remember { mutableStateOf(false) } - val context = LocalContext.current - LaunchedEffect(viewModel.navigateTo) { when (viewModel.navigateTo) { "mfa" -> { @@ -189,21 +190,53 @@ fun LoginScreen(navController: NavController, viewModel: LoginViewModel = hiltVi } } - Column( - modifier = Modifier - .fillMaxSize() - .padding(20.dp) - .imePadding() - .safeDrawingPadding(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { + Scaffold( + topBar = { + Column { + TopAppBar( + modifier = Modifier.fillMaxWidth() + .padding(horizontal = 16.dp), + title = {}, + actions = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton( + content = { + Icon( + painter = painterResource(R.drawable.icn_arrow_back_24dp), + contentDescription = stringResource(R.string.back) + ) + }, + onClick = { navController.popBackStack() } + ) + Text(text = "Register") + } + }, + ) + HorizontalDivider() + } + } + ) { innerPadding -> Column( modifier = Modifier - .weight(1f), - verticalArrangement = Arrangement.Center, + .padding(innerPadding) + .fillMaxSize() + .padding(vertical = 20.dp, horizontal = 16.dp) + .imePadding(), + verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally ) { + Spacer(modifier = Modifier.height(16.dp)) + Image( + modifier = Modifier + .size(120.dp), + painter = painterResource(R.drawable.login_charachter_img), + contentDescription = "Login character" + ) + Spacer(modifier = Modifier.height(4.dp)) Text( text = stringResource(R.string.login_heading), style = MaterialTheme.typography.displaySmall.copy( @@ -212,15 +245,31 @@ fun LoginScreen(navController: NavController, viewModel: LoginViewModel = hiltVi textAlign = TextAlign.Center ), modifier = Modifier - .padding(horizontal = 20.dp, vertical = 10.dp) + .padding(horizontal = 20.dp, vertical = 4.dp) + .fillMaxWidth() + ) + Text( + text = stringResource(R.string.login_subheading), + color = MaterialTheme.colorScheme.onBackground.copy( + alpha = 0.5f + ), + style = MaterialTheme.typography.titleMedium.copy( + fontSize = 16.sp, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Normal + ), + modifier = Modifier + .padding(horizontal = 20.dp) .fillMaxWidth() ) + Spacer(modifier = Modifier.height(32.dp)) + Column( modifier = Modifier - .width(270.dp), + .fillMaxWidth(), verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.Start ) { FormTextField( value = viewModel.email, @@ -229,7 +278,8 @@ fun LoginScreen(navController: NavController, viewModel: LoginViewModel = hiltVi action = ImeAction.Next, onChange = viewModel::setEmail, modifier = Modifier - .padding(vertical = 25.dp) + .fillMaxWidth() + .padding(bottom = 16.dp) .semantics { contentType = ContentType.EmailAddress } @@ -250,6 +300,7 @@ fun LoginScreen(navController: NavController, viewModel: LoginViewModel = hiltVi showPassword.value = !showPassword.value }) { when { + showPassword.value -> { Icon( painter = painterResource(R.drawable.icn_visibility_off_24dp), @@ -266,59 +317,27 @@ fun LoginScreen(navController: NavController, viewModel: LoginViewModel = hiltVi } } }, - modifier = Modifier.semantics { - contentType = ContentType.Password - } + modifier = Modifier + .fillMaxWidth() + .semantics { + contentType = ContentType.Password + } ) - Weblink( text = stringResource(R.string.password_forgot), url = "$REVOLT_APP/login/reset", - modifier = Modifier.padding(vertical = 7.dp) + modifier = Modifier.padding(vertical = 12.dp) ) + Spacer(modifier = Modifier.height(32.dp)) - if (viewModel.error != null) { - Text( - text = viewModel.error!!, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.titleMedium.copy( - textAlign = TextAlign.Center, - fontWeight = FontWeight.Normal, - fontSize = 15.sp - ), - modifier = Modifier.padding(vertical = 7.dp) - ) - } - } - } - - Column( - modifier = Modifier - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Weblink( - text = stringResource(R.string.resend_verification), - url = "$REVOLT_APP/login/resend", - modifier = Modifier - .padding(vertical = 7.dp) - .testTag("resend_verification_link") - ) - - Spacer(modifier = Modifier.height(10.dp)) - - Row { - TextButton(onClick = { - navController.popBackStack() - }) { - Text(text = stringResource(R.string.back)) - } - - Spacer(modifier = Modifier.width(10.dp)) - - Button(onClick = { - viewModel.doLogin() - }) { + Button( + onClick = { + viewModel.doLogin() + }, + modifier = Modifier + .fillMaxWidth() + .testTag("confirm_platform_button") + ) { Text(text = stringResource(R.string.login)) } } diff --git a/app/src/main/java/chat/revolt/screens/register/RegisterDetailsScreen.kt b/app/src/main/java/chat/revolt/screens/register/RegisterDetailsScreen.kt index 1355f9c2..bd0ab8d0 100644 --- a/app/src/main/java/chat/revolt/screens/register/RegisterDetailsScreen.kt +++ b/app/src/main/java/chat/revolt/screens/register/RegisterDetailsScreen.kt @@ -1,6 +1,7 @@ package chat.revolt.screens.register import android.content.Context +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -10,12 +11,16 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.size import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -24,6 +29,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.autofill.ContentType import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentType import androidx.compose.ui.semantics.semantics @@ -112,28 +119,59 @@ class RegisterDetailsScreenViewModel : ViewModel() { } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun RegisterDetailsScreen( navController: NavController, viewModel: RegisterDetailsScreenViewModel = viewModel() ) { - val context = LocalContext.current - - Column( - modifier = Modifier - .fillMaxSize() - .padding(20.dp) - .imePadding() - .safeDrawingPadding(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { + Scaffold( + topBar = { + Column { + TopAppBar( + modifier = Modifier.fillMaxWidth() + .padding(horizontal = 16.dp), + title = {}, + actions = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton( + content = { + Icon( + painter = painterResource(R.drawable.icn_arrow_back_24dp), + contentDescription = stringResource(R.string.back) + ) + }, + onClick = { navController.popBackStack() } + ) + Text(text = stringResource(R.string.login)) + } + }, + ) + HorizontalDivider() + } + } + ) { innerPadding -> Column( modifier = Modifier - .weight(1f), - verticalArrangement = Arrangement.Center, + .padding(innerPadding) + .fillMaxSize() + .padding(vertical = 20.dp, horizontal = 16.dp) + .imePadding(), + verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally ) { + Spacer(modifier = Modifier.height(16.dp)) + Image( + modifier = Modifier + .size(120.dp), + painter = painterResource(R.drawable.login_charachter_img), + contentDescription = "Login character" + ) + Spacer(modifier = Modifier.height(4.dp)) Text( text = stringResource(R.string.register_form_heading), style = MaterialTheme.typography.displaySmall.copy( @@ -142,33 +180,31 @@ fun RegisterDetailsScreen( textAlign = TextAlign.Center ), modifier = Modifier - .padding(horizontal = 10.dp) + .padding(horizontal = 20.dp, vertical = 4.dp) .fillMaxWidth() ) - - Spacer(modifier = Modifier.height(10.dp)) - Text( text = stringResource(R.string.register_data), color = MaterialTheme.colorScheme.onBackground.copy( alpha = 0.5f ), style = MaterialTheme.typography.titleMedium.copy( + fontSize = 16.sp, textAlign = TextAlign.Center, fontWeight = FontWeight.Normal ), modifier = Modifier - .padding(horizontal = 10.dp) + .padding(horizontal = 20.dp) .fillMaxWidth() ) - Spacer(modifier = Modifier.height(40.dp)) + Spacer(modifier = Modifier.height(32.dp)) Column( modifier = Modifier .fillMaxWidth(), verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.Start ) { FormTextField( value = viewModel.email, @@ -176,66 +212,34 @@ fun RegisterDetailsScreen( label = stringResource(R.string.register_email), type = KeyboardType.Email, action = ImeAction.Next, - modifier = Modifier.semantics { contentType = ContentType.EmailAddress } + modifier = Modifier.fillMaxWidth().semantics { contentType = ContentType.EmailAddress } ) - Text( - text = stringResource(R.string.register_email_verification_hint), - color = MaterialTheme.colorScheme.onBackground.copy( - alpha = 0.5f - ), - fontSize = 12.sp, - modifier = Modifier.padding(horizontal = 40.dp, vertical = 10.dp) - ) - - Spacer(modifier = Modifier.height(10.dp)) - + Spacer(modifier = Modifier.height(16.dp)) FormTextField( value = viewModel.password, onChange = { viewModel.password = it }, label = stringResource(R.string.register_password), - type = KeyboardType.Password, - action = ImeAction.Done, - modifier = Modifier.semantics { contentType = ContentType.NewPassword } - ) - Text( - text = stringResource(R.string.register_password_rules), - color = MaterialTheme.colorScheme.onBackground.copy( - alpha = 0.5f - ), - fontSize = 12.sp, - modifier = Modifier.padding(horizontal = 40.dp, vertical = 10.dp) + type = KeyboardType.Email, + action = ImeAction.Next, + modifier = Modifier.fillMaxWidth().semantics { contentType = ContentType.EmailAddress } ) + Spacer(modifier = Modifier.height(32.dp)) - if (!viewModel.error.isNullOrBlank()) { - Text( - text = viewModel.error!!, - color = MaterialTheme.colorScheme.error, - modifier = Modifier.padding(horizontal = 40.dp, vertical = 10.dp), - textAlign = TextAlign.Center - ) + val context = LocalContext.current + + Button( + onClick = { + viewModel.initCaptcha(context) { + viewModel.doRegistration(navController) + } + }, + modifier = Modifier + .fillMaxWidth() + .testTag("setup_continue_button") + ) { + Text(text = stringResource(R.string.continue_)) } } } - - Row { - TextButton(onClick = { - navController.popBackStack() - }) { - Text(text = stringResource(R.string.back)) - } - - Spacer(modifier = Modifier.width(10.dp)) - - Button( - onClick = { - viewModel.initCaptcha(context) { - viewModel.doRegistration(navController) - } - }, - enabled = viewModel.email.isNotBlank() && viewModel.password.isNotBlank() - ) { - Text(text = stringResource(R.string.signup)) - } - } } } diff --git a/app/src/main/res/drawable/login_charachter_img.xml b/app/src/main/res/drawable/login_charachter_img.xml new file mode 100644 index 00000000..c517707b --- /dev/null +++ b/app/src/main/res/drawable/login_charachter_img.xml @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bccf2571..7febdc6e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,6 +8,7 @@ Share Let\'s go Confirm + Continue Fetching some info, hang in there… Hold your horses! You\'re doing that too often in a short amount of time. @@ -39,7 +40,8 @@ Try again Log out - Let\'s log you in + Welcome back! + Great to see you again! Forgot password? Resend a verification email Using a password manager? @@ -49,8 +51,8 @@ Welcome to Revolt Use Revolt to chat about stuff. It\'s like a place to hang out, get together, and talk about stuff. Best part, it\'s on the internet. - Let\'s get you set up. - Your data is safe with us. We don\'t sell it, and neither do we use it to show you ads. + Make it Official! + New Here? Let’s Get You Set Up! Email We\'ll send you a verification email to confirm your account. Password