parent
171586c312
commit
d8953a4eb6
|
|
@ -0,0 +1,177 @@
|
|||
package chat.revolt.components.generic
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.revolt.R
|
||||
|
||||
sealed class DobElement {
|
||||
data object Day : DobElement()
|
||||
data object Month : DobElement()
|
||||
data object Year : DobElement()
|
||||
}
|
||||
|
||||
object DobRegion {
|
||||
val American = listOf(DobElement.Month, DobElement.Day, DobElement.Year) to "/"
|
||||
val British = listOf(DobElement.Day, DobElement.Month, DobElement.Year) to "/"
|
||||
val European = listOf(DobElement.Day, DobElement.Month, DobElement.Year) to "."
|
||||
val ISO2601 = listOf(DobElement.Year, DobElement.Month, DobElement.Day) to "-"
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DobPicker(
|
||||
dayValue: String,
|
||||
monthValue: String,
|
||||
yearValue: String,
|
||||
onDayChange: (String) -> Unit,
|
||||
onMonthChange: (String) -> Unit,
|
||||
onYearChange: (String) -> Unit,
|
||||
order: List<DobElement> = DobRegion.European.first,
|
||||
separator: String = DobRegion.European.second,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
order.forEach { element ->
|
||||
when (element) {
|
||||
DobElement.Day -> DobPickerDay(dayValue, onDayChange)
|
||||
DobElement.Month -> DobPickerMonth(monthValue, onMonthChange)
|
||||
DobElement.Year -> DobPickerYear(yearValue, onYearChange)
|
||||
}
|
||||
|
||||
if (element != order.last()) {
|
||||
Text(
|
||||
text = separator,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.offset(y = 12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DobPickerDay(
|
||||
dayValue: String,
|
||||
onDayChange: (String) -> Unit,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.channel_age_gate_dob_day),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = dayValue,
|
||||
onValueChange = {
|
||||
if (it.length <= 2) {
|
||||
onDayChange(it)
|
||||
} else {
|
||||
focusManager.moveFocus(FocusDirection.Next)
|
||||
}
|
||||
|
||||
if (it.length == 2) {
|
||||
focusManager.moveFocus(FocusDirection.Next)
|
||||
}
|
||||
},
|
||||
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||
fontFeatureSettings = "tnum",
|
||||
textAlign = TextAlign.Center
|
||||
),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.width(56.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DobPickerMonth(
|
||||
monthValue: String,
|
||||
onMonthChange: (String) -> Unit,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.channel_age_gate_dob_month),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = monthValue,
|
||||
onValueChange = {
|
||||
if (it.length <= 2) {
|
||||
onMonthChange(it)
|
||||
}
|
||||
|
||||
if (it.length == 2) {
|
||||
focusManager.moveFocus(FocusDirection.Next)
|
||||
} else if (it.isEmpty()) {
|
||||
focusManager.moveFocus(FocusDirection.Previous)
|
||||
}
|
||||
},
|
||||
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||
fontFeatureSettings = "tnum",
|
||||
textAlign = TextAlign.Center
|
||||
),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.width(56.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DobPickerYear(
|
||||
yearValue: String,
|
||||
onYearChange: (String) -> Unit,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.channel_age_gate_dob_year),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = yearValue,
|
||||
onValueChange = {
|
||||
onYearChange(it)
|
||||
if (it.isEmpty()) {
|
||||
focusManager.moveFocus(FocusDirection.Previous)
|
||||
} else if (it.length == 4) {
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
},
|
||||
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||
fontFeatureSettings = "tnum",
|
||||
textAlign = TextAlign.Center
|
||||
),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.width(78.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -305,165 +305,187 @@ fun ChannelScreen(
|
|||
}
|
||||
|
||||
LaunchedEffect(viewModel.activeChannel, RevoltAPI.channelCache, RevoltAPI.serverCache) {
|
||||
viewModel.checkShouldDenyMessageField()
|
||||
viewModel.doInitialChecks()
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.weight(1f),
|
||||
contentAlignment = Alignment.BottomEnd
|
||||
) {
|
||||
LazyColumn(state = lazyListState, reverseLayout = true) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(25.dp))
|
||||
}
|
||||
Crossfade(targetState = viewModel.showAgeGate, label = "Age gate shown/hidden") {
|
||||
if (it) {
|
||||
ChannelScreenAgeGate(
|
||||
onAccept = {
|
||||
viewModel.showAgeGate = false
|
||||
},
|
||||
onDeny = {
|
||||
onToggleDrawer()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
LazyColumn(state = lazyListState, reverseLayout = true) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(25.dp))
|
||||
}
|
||||
|
||||
items(
|
||||
items = viewModel.renderableMessages,
|
||||
key = { it.id!! }
|
||||
) { message ->
|
||||
when {
|
||||
message.system != null -> SystemMessage(message)
|
||||
else -> Message(
|
||||
message,
|
||||
parse = {
|
||||
val parser = MarkdownParser()
|
||||
.addRules(
|
||||
SimpleMarkdownRules.createEscapeRule()
|
||||
)
|
||||
.addRevoltRules(context)
|
||||
.addRules(
|
||||
createCodeRule(context, codeBlockColor.toArgb()),
|
||||
createInlineCodeRule(context, codeBlockColor.toArgb())
|
||||
)
|
||||
.addRules(
|
||||
SimpleMarkdownRules.createSimpleMarkdownRules(
|
||||
includeEscapeRule = false
|
||||
)
|
||||
)
|
||||
|
||||
SimpleRenderer.render(
|
||||
source = it.content ?: "",
|
||||
parser = parser,
|
||||
initialState = MarkdownState(0),
|
||||
renderContext = MarkdownContext(
|
||||
memberMap = viewModel.activeChannel?.server?.let { serverId ->
|
||||
RevoltAPI.members.markdownMemberMapFor(
|
||||
serverId
|
||||
items(
|
||||
items = viewModel.renderableMessages,
|
||||
key = { it.id!! }
|
||||
) { message ->
|
||||
when {
|
||||
message.system != null -> SystemMessage(message)
|
||||
else -> Message(
|
||||
message,
|
||||
parse = {
|
||||
val parser = MarkdownParser()
|
||||
.addRules(
|
||||
SimpleMarkdownRules.createEscapeRule()
|
||||
)
|
||||
} ?: mapOf(),
|
||||
userMap = RevoltAPI.userCache.toMap(),
|
||||
channelMap = RevoltAPI.channelCache.mapValues { ch ->
|
||||
ch.value.name ?: ch.value.id ?: "#DeletedChannel"
|
||||
},
|
||||
emojiMap = RevoltAPI.emojiCache,
|
||||
serverId = channel?.server ?: "",
|
||||
// check if message consists solely of one *or more* custom emotes
|
||||
useLargeEmojis = it.content?.matches(
|
||||
Regex("(:([0-9A-Z]{26}):)+")
|
||||
) == true
|
||||
.addRevoltRules(context)
|
||||
.addRules(
|
||||
createCodeRule(context, codeBlockColor.toArgb()),
|
||||
createInlineCodeRule(
|
||||
context,
|
||||
codeBlockColor.toArgb()
|
||||
)
|
||||
)
|
||||
.addRules(
|
||||
SimpleMarkdownRules.createSimpleMarkdownRules(
|
||||
includeEscapeRule = false
|
||||
)
|
||||
)
|
||||
|
||||
SimpleRenderer.render(
|
||||
source = it.content ?: "",
|
||||
parser = parser,
|
||||
initialState = MarkdownState(0),
|
||||
renderContext = MarkdownContext(
|
||||
memberMap = viewModel.activeChannel?.server?.let { serverId ->
|
||||
RevoltAPI.members.markdownMemberMapFor(
|
||||
serverId
|
||||
)
|
||||
} ?: mapOf(),
|
||||
userMap = RevoltAPI.userCache.toMap(),
|
||||
channelMap = RevoltAPI.channelCache.mapValues { ch ->
|
||||
ch.value.name ?: ch.value.id
|
||||
?: "#DeletedChannel"
|
||||
},
|
||||
emojiMap = RevoltAPI.emojiCache,
|
||||
serverId = channel?.server ?: "",
|
||||
// check if message consists solely of one *or more* custom emotes
|
||||
useLargeEmojis = it.content?.matches(
|
||||
Regex("(:([0-9A-Z]{26}):)+")
|
||||
) == true
|
||||
)
|
||||
)
|
||||
},
|
||||
onMessageContextMenu = {
|
||||
messageContextSheetShown = true
|
||||
messageContextSheetTarget = message.id ?: ""
|
||||
},
|
||||
onAvatarClick = {
|
||||
message.author?.let { author ->
|
||||
onUserSheetOpenFor(author, channel?.server)
|
||||
}
|
||||
},
|
||||
onNameClick = {
|
||||
val author = message.author?.let { RevoltAPI.userCache[it] }
|
||||
?: return@Message
|
||||
viewModel.putAtCursorPosition("@${author.username}#${author.discriminator}")
|
||||
},
|
||||
canReply = true,
|
||||
onReply = {
|
||||
if (viewModel.pendingReplies.size >= 5) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.too_many_replies, 5),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return@Message
|
||||
}
|
||||
viewModel.replyToMessage(message)
|
||||
},
|
||||
onAddReaction = {
|
||||
message.id?.let {
|
||||
reactSheetShown = true
|
||||
reactSheetTarget = it
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
if (viewModel.hasNoMoreMessages) {
|
||||
Text(
|
||||
text = stringResource(R.string.start_of_conversation),
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = 8.dp,
|
||||
end = 8.dp,
|
||||
top = 64.dp,
|
||||
bottom = 32.dp
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
!isScrolledToBottom.value,
|
||||
enter = slideInHorizontally(
|
||||
animationSpec = RevoltTweenInt,
|
||||
initialOffsetX = { it }
|
||||
) + fadeIn(animationSpec = RevoltTweenFloat),
|
||||
exit = slideOutHorizontally(
|
||||
animationSpec = RevoltTweenInt,
|
||||
targetOffsetX = { it }
|
||||
) + fadeOut(animationSpec = RevoltTweenFloat),
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
modifier = Modifier
|
||||
.padding(bottom = scrollDownFABPadding)
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(16.dp),
|
||||
text = {
|
||||
Text(stringResource(R.string.scroll_to_bottom))
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = stringResource(R.string.scroll_to_bottom)
|
||||
)
|
||||
},
|
||||
onMessageContextMenu = {
|
||||
messageContextSheetShown = true
|
||||
messageContextSheetTarget = message.id ?: ""
|
||||
},
|
||||
onAvatarClick = {
|
||||
message.author?.let { author ->
|
||||
onUserSheetOpenFor(author, channel?.server)
|
||||
}
|
||||
},
|
||||
onNameClick = {
|
||||
val author = message.author?.let { RevoltAPI.userCache[it] }
|
||||
?: return@Message
|
||||
viewModel.putAtCursorPosition("@${author.username}#${author.discriminator}")
|
||||
},
|
||||
canReply = true,
|
||||
onReply = {
|
||||
if (viewModel.pendingReplies.size >= 5) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.too_many_replies, 5),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return@Message
|
||||
}
|
||||
viewModel.replyToMessage(message)
|
||||
},
|
||||
onAddReaction = {
|
||||
message.id?.let {
|
||||
reactSheetShown = true
|
||||
reactSheetTarget = it
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
lazyListState.animateScrollToItem(0)
|
||||
}
|
||||
},
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
if (viewModel.hasNoMoreMessages) {
|
||||
Text(
|
||||
text = stringResource(R.string.start_of_conversation),
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp, end = 8.dp, top = 64.dp, bottom = 32.dp)
|
||||
.fillMaxWidth(),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
TypingIndicator(
|
||||
users = viewModel.typingUsers
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
!isScrolledToBottom.value,
|
||||
enter = slideInHorizontally(
|
||||
animationSpec = RevoltTweenInt,
|
||||
initialOffsetX = { it }
|
||||
) + fadeIn(animationSpec = RevoltTweenFloat),
|
||||
exit = slideOutHorizontally(
|
||||
animationSpec = RevoltTweenInt,
|
||||
targetOffsetX = { it }
|
||||
) + fadeOut(animationSpec = RevoltTweenFloat),
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
modifier = Modifier
|
||||
.padding(bottom = scrollDownFABPadding)
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(16.dp),
|
||||
text = {
|
||||
Text(stringResource(R.string.scroll_to_bottom))
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = stringResource(R.string.scroll_to_bottom)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
lazyListState.animateScrollToItem(0)
|
||||
}
|
||||
},
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
TypingIndicator(
|
||||
users = viewModel.typingUsers
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
|
|
@ -550,7 +572,7 @@ fun ChannelScreen(
|
|||
R.string.unknown
|
||||
),
|
||||
forceSendButton = viewModel.pendingAttachments.isNotEmpty(),
|
||||
disabled = viewModel.pendingAttachments.isNotEmpty() && viewModel.isSendingMessage,
|
||||
disabled = (viewModel.pendingAttachments.isNotEmpty() && viewModel.isSendingMessage) || viewModel.showAgeGate,
|
||||
channelId = channelId,
|
||||
serverId = channel?.server,
|
||||
editMode = viewModel.editingMessage != null,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
package chat.revolt.screens.chat.views.channel
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
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.components.generic.DobPicker
|
||||
import chat.revolt.components.generic.DobRegion
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.UtcOffset
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
|
||||
@Composable
|
||||
fun ChannelScreenAgeGate(
|
||||
onAccept: () -> Unit,
|
||||
onDeny: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var dayValue by remember { mutableStateOf("") }
|
||||
var monthValue by remember { mutableStateOf("") }
|
||||
var yearValue by remember { mutableStateOf("") }
|
||||
|
||||
var dobPickerError by remember { mutableStateOf("") }
|
||||
var dobValid by remember { mutableStateOf(false) }
|
||||
var dobPickerRegion by remember { mutableStateOf(DobRegion.European) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
dobPickerRegion = when (Locale.current.region) {
|
||||
"US" -> DobRegion.American
|
||||
"GB" -> DobRegion.British
|
||||
"JP", "KR", "CN" -> DobRegion.ISO2601
|
||||
else -> DobRegion.European
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(dayValue, monthValue, yearValue) {
|
||||
dobValid = false
|
||||
if (dayValue.isNotBlank() && monthValue.isNotBlank() && yearValue.isNotBlank()) {
|
||||
val day = dayValue.toIntOrNull()
|
||||
val month = monthValue.toIntOrNull()
|
||||
val year = yearValue.toIntOrNull()
|
||||
|
||||
// Invalid condition
|
||||
if (day == null || month == null || year == null) {
|
||||
dobPickerError = context.getString(R.string.channel_age_gate_dob_invalid)
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
val then: LocalDateTime
|
||||
|
||||
// Invalid date condition
|
||||
try {
|
||||
then = LocalDateTime(year, month, day, 0, 0, 0)
|
||||
} catch (e: Throwable) {
|
||||
dobPickerError = context.getString(R.string.channel_age_gate_dob_invalid)
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
// Too young condition (< 18)
|
||||
val diff =
|
||||
now.toInstant(UtcOffset.ZERO).epochSeconds - then.toInstant(UtcOffset.ZERO).epochSeconds
|
||||
if (diff < 568025136) {
|
||||
dobPickerError = context.getString(R.string.channel_age_gate_dob_too_young)
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
// Born in future condition
|
||||
if (then.toInstant(UtcOffset.ZERO).epochSeconds > now.toInstant(UtcOffset.ZERO).epochSeconds) {
|
||||
dobPickerError = context.getString(R.string.channel_age_gate_dob_invalid_future)
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
// Born before oldest person condition
|
||||
// Update from https://en.wikipedia.org/wiki/List_of_oldest_living_people
|
||||
val minYob = 1907
|
||||
if (year < minYob) {
|
||||
dobPickerError =
|
||||
context.getString(R.string.channel_age_gate_dob_invalid_unlikely, minYob)
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
// Success condition
|
||||
dobPickerError = ""
|
||||
dobValid = true
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_comment_alert_24dp),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.channel_age_gate_title),
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
textAlign = TextAlign.Center
|
||||
),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.channel_age_gate_description),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
textAlign = TextAlign.Center
|
||||
),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = dobPickerError,
|
||||
style = MaterialTheme.typography.labelMedium.copy(
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center
|
||||
),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.channel_age_gate_dob_section),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
|
||||
DobPicker(
|
||||
dayValue = dayValue,
|
||||
monthValue = monthValue,
|
||||
yearValue = yearValue,
|
||||
onDayChange = { dayValue = it },
|
||||
onMonthChange = { monthValue = it },
|
||||
onYearChange = { yearValue = it },
|
||||
order = dobPickerRegion.first,
|
||||
separator = dobPickerRegion.second,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(0.dp))
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
|
||||
) {
|
||||
Button(onClick = { onDeny() }) {
|
||||
Text(stringResource(R.string.channel_age_gate_dob_cancel))
|
||||
}
|
||||
|
||||
TextButton(onClick = { onAccept() }, enabled = dobValid) {
|
||||
Text(stringResource(R.string.channel_age_gate_dob_proceed))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AgeGateScreenPreview() {
|
||||
ChannelScreenAgeGate(onAccept = { /*TODO*/ }, onDeny = { /*TODO*/ })
|
||||
}
|
||||
|
|
@ -86,6 +86,8 @@ class ChannelScreenViewModel : ViewModel() {
|
|||
var denyMessageField by mutableStateOf(false)
|
||||
var denyMessageFieldReasonResource by mutableIntStateOf(R.string.message_field_denied_generic)
|
||||
|
||||
var showAgeGate by mutableStateOf(false)
|
||||
|
||||
private fun popAttachmentBatch() {
|
||||
pendingAttachments =
|
||||
pendingAttachments.drop(MAX_ATTACHMENTS_PER_MESSAGE).toMutableStateList()
|
||||
|
|
@ -518,7 +520,17 @@ class ChannelScreenViewModel : ViewModel() {
|
|||
currentSelection.first + content.length to currentSelection.first + content.length
|
||||
}
|
||||
|
||||
suspend fun checkShouldDenyMessageField() {
|
||||
suspend fun doInitialChecks() {
|
||||
checkShouldShowAgeGate()
|
||||
checkShouldDenyMessageField()
|
||||
}
|
||||
|
||||
private fun checkShouldShowAgeGate() {
|
||||
if (activeChannel == null) return
|
||||
showAgeGate = activeChannel!!.nsfw == true
|
||||
}
|
||||
|
||||
private suspend fun checkShouldDenyMessageField() {
|
||||
if (activeChannel == null) return
|
||||
|
||||
val selfUser = RevoltAPI.userCache[RevoltAPI.selfId] ?: return
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M9 22C8.4 22 8 21.6 8 21V18H4C2.9 18 2 17.1 2 16V4C2 2.9 2.9 2 4 2H20C21.1 2 22 2.9 22 4V16C22 17.1 21.1 18 20 18H13.9L10.2 21.7C10 21.9 9.8 22 9.5 22H9M13 11V5H11V11M13 15V13H11V15H13Z" />
|
||||
</vector>
|
||||
|
|
@ -138,6 +138,19 @@
|
|||
<string name="no_active_channel">You\'re not in a channel right now.</string>
|
||||
<string name="no_active_channel_body">Select a server from the left to get started. If you\'re feeling adventurous, you can create a new server.</string>
|
||||
|
||||
<string name="channel_age_gate_title">This channel is marked as NSFW</string>
|
||||
<string name="channel_age_gate_description">You must be 18 or older to view this channel.</string>
|
||||
<string name="channel_age_gate_dob_section">Date of birth</string>
|
||||
<string name="channel_age_gate_dob_day">Day</string>
|
||||
<string name="channel_age_gate_dob_month">Month</string>
|
||||
<string name="channel_age_gate_dob_year">Year</string>
|
||||
<string name="channel_age_gate_dob_proceed">Proceed</string>
|
||||
<string name="channel_age_gate_dob_cancel">Cancel</string>
|
||||
<string name="channel_age_gate_dob_invalid">Enter your date of birth</string>
|
||||
<string name="channel_age_gate_dob_too_young">You must be 18 or older to view this channel.</string>
|
||||
<string name="channel_age_gate_dob_invalid_future">You can\'t be born in the future.</string>
|
||||
<string name="channel_age_gate_dob_invalid_unlikely">You can\'t be born before %1$d.</string>
|
||||
|
||||
<string name="avatar_alt">%1$s\'s avatar</string>
|
||||
|
||||
<string name="direct_messages">Direct Messages</string>
|
||||
|
|
|
|||
Loading…
Reference in New Issue