From a92a07e2ed4788edb345d8c8207b108e95f8aad0 Mon Sep 17 00:00:00 2001 From: Infi Date: Sat, 26 Jul 2025 07:12:29 +0200 Subject: [PATCH] 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