ServerSheet: Add notification settings

This commit introduces a new page to the server context sheet that allows users to manage their notification settings for the server.

Users can now choose between the following notification options:
- Use default
- Muted
- All messages
- Mentions only
- None

The sheet now uses an `AnimatedContent` composable to switch between the main server information page and the new notification settings page.
This commit is contained in:
AbronStudio 2025-08-20 13:50:40 +03:30
parent 85c807a5c6
commit d49a63a80e
1 changed files with 301 additions and 149 deletions

View File

@ -3,6 +3,12 @@ package chat.peptide.sheets
import android.content.Intent
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -11,6 +17,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
@ -22,8 +29,10 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.RadioButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -44,6 +53,9 @@ import androidx.core.net.toUri
import chat.peptide.R
import chat.peptide.api.PeptideAPI
import chat.peptide.api.routes.server.leaveOrDeleteServer
import chat.peptide.api.schemas.NotificationSettings
import chat.peptide.api.settings.NotificationType
import chat.peptide.api.settings.SyncedSettings
import chat.peptide.composables.generic.PepTextButton
import chat.peptide.composables.generic.SheetButton
import chat.peptide.composables.generic.SquareButton
@ -54,6 +66,7 @@ import chat.peptide.callbacks.Action
import chat.peptide.callbacks.ActionChannel
import chat.peptide.internals.Platform
import chat.peptide.screens.chat.ChatRouterDestination
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@Composable
@ -81,6 +94,7 @@ fun ServerContextSheet(
return
}
var page by remember { mutableIntStateOf(0) }
if (showLeaveConfirmation) {
AlertDialog(
@ -160,167 +174,305 @@ fun ServerContextSheet(
)
}
Column(Modifier.verticalScroll(rememberScrollState())) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 4.dp),
) {
ServerOverview(server)
AnimatedContent(
targetState = page,
transitionSpec = {
if (targetState > initialState) {
slideInHorizontally(animationSpec = tween(200)) { it } togetherWith
slideOutHorizontally(animationSpec = tween(200)) { -it }
} else {
slideInHorizontally(animationSpec = tween(200)) { -it } togetherWith
slideOutHorizontally(animationSpec = tween(200)) { it }
}
}, label = "Server context pager"
) { current ->
if (current == 0) {
Column(Modifier.verticalScroll(rememberScrollState())) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 4.dp),
) {
ServerOverview(server)
SelectionContainer {
RichMarkdown(
input = if (server.description?.isBlank() == false) {
server.description
} else {
stringResource(
R.string.server_context_sheet_description_empty
SelectionContainer {
RichMarkdown(
input = if (server.description?.isBlank() == false) {
server.description
} else {
stringResource(
R.string.server_context_sheet_description_empty
)
}
)
}
)
}
if (server.owner == PeptideAPI.selfId) {
if (server.owner == PeptideAPI.selfId) {
Box(
modifier = Modifier
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.primary)
) {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimary) {
SheetSelection(
icon = {},
title = {
Text(
text = stringResource(id = R.string.server_context_sheet_moderators_early_disclaimer_title)
)
},
description = {
Text(
text = stringResource(id = R.string.server_context_sheet_moderators_early_disclaimer_body)
)
},
arrowTint = LocalContentColor.current.copy(alpha = 0.5f),
) {
context.startActivity(
Intent(
Intent.ACTION_VIEW,
"${PeptideAPI.getCurrentAppUrl()}/server/${server.id}/settings".toUri()
)
)
}
}
}
}
Spacer(modifier = Modifier.height(12.dp))
}
// Primary actions group
Column(
modifier = Modifier
.padding(horizontal = 12.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.surfaceBright)
) {
SheetButton(
leadingContent = {
Icon(
painter = painterResource(id = R.drawable.icn_identifier_copy_24dp),
contentDescription = null
)
},
headlineContent = {
Text(
text = stringResource(id = R.string.server_context_sheet_actions_copy_id)
)
},
onClick = {
if (server.id == null) return@SheetButton
clipboardManager.setText(AnnotatedString(server.id))
if (Platform.needsShowClipboardNotification()) {
Toast.makeText(
context,
context.getString(R.string.server_context_sheet_actions_copy_id_copied),
Toast.LENGTH_SHORT
).show()
}
coroutineScope.launch {
onHideSheet()
}
}
)
HorizontalDivider()
SheetButton(
leadingContent = {
Icon(
painter = painterResource(R.drawable.icn_notification_settings_24dp),
contentDescription = null
)
},
headlineContent = {
Text(
text = stringResource(id = R.string.channel_info_sheet_options_notifications_manage)
)
},
onClick = { page = 1 }
)
HorizontalDivider()
SheetButton(
leadingContent = {
Icon(
painter = painterResource(id = R.drawable.icn_mark_chat_read_24dp),
contentDescription = null
)
},
headlineContent = {
Text(
text = stringResource(id = R.string.server_context_sheet_actions_mark_read)
)
},
onClick = {
coroutineScope.launch {
server.id?.let {
PeptideAPI.unreads.markServerAsRead(it, sync = true)
}
onHideSheet()
}
}
)
}
// Danger actions group
if (server.owner != PeptideAPI.selfId) {
Column(
modifier = Modifier
.padding(start = 12.dp, end = 12.dp, top = 16.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.surfaceBright)
) {
SheetButton(
leadingContent = {
Icon(
painter = painterResource(id = R.drawable.icn_report_24dp),
contentDescription = null
)
},
headlineContent = {
Text(
text = stringResource(id = R.string.server_context_sheet_actions_report),
)
},
dangerous = true,
onClick = {
onReportServer()
}
)
HorizontalDivider()
SheetButton(
leadingContent = {
Icon(
painter = painterResource(id = R.drawable.icn_door_open_24dp),
contentDescription = null,
)
},
headlineContent = {
Text(
text = stringResource(id = R.string.server_context_sheet_actions_leave)
)
},
dangerous = true,
onClick = {
showLeaveConfirmation = true
}
)
}
}
}
} else {
Column(Modifier.verticalScroll(rememberScrollState())) {
Box(
modifier = Modifier
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.primary)
.fillMaxWidth()
.padding(12.dp),
) {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimary) {
SheetSelection(
icon = {},
title = {
Text(
text = stringResource(id = R.string.server_context_sheet_moderators_early_disclaimer_title)
)
Icon(
painter = painterResource(id = R.drawable.ic_arrow_left_24dp),
contentDescription = null,
modifier = Modifier
.size(24.dp)
.clickable { page = 0 }
.align(Alignment.CenterStart)
)
Text(
modifier = Modifier.align(Alignment.Center),
text = stringResource(id = R.string.channel_info_sheet_options_notifications_manage),
style = MaterialTheme.typography.titleMedium
)
}
Column(
modifier = Modifier
.padding(top = 24.dp, start = 12.dp, end = 12.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.surfaceBright)
) {
var selected by remember {
mutableStateOf(
NotificationType.fromStorage(
SyncedSettings.notifications.server[server.id ?: ""]
).storageValue
)
}
fun updateSelection(newValue: NotificationType) {
selected = newValue.storageValue
val currentSettings = SyncedSettings.notifications
val updated = NotificationSettings(
server = currentSettings.server.toMutableMap().apply {
if (server.id.isNullOrBlank() || newValue == NotificationType.DEFAULT) {
if (!server.id.isNullOrBlank()) remove(server.id)
} else {
put(server.id!!, newValue.storageValue)
}
},
description = {
Text(
text = stringResource(id = R.string.server_context_sheet_moderators_early_disclaimer_body)
)
},
arrowTint = LocalContentColor.current.copy(alpha = 0.5f),
) {
context.startActivity(
Intent(
Intent.ACTION_VIEW,
"${PeptideAPI.getCurrentAppUrl()}/server/${server.id}/settings".toUri()
)
)
channel = currentSettings.channel
)
GlobalScope.launch {
SyncedSettings.updateNotifications(updated)
}
}
@Composable
fun ServerNotificationRow(title: String, type: NotificationType) {
val isSelected = selected == type.storageValue
SheetButton(
headlineContent = { Text(text = title) },
leadingContent = {
Icon(
painter = painterResource(
id = when (type) {
NotificationType.DEFAULT -> R.drawable.icn_notif_use_default
NotificationType.MUTED -> R.drawable.icn_notif_mute
NotificationType.ALL -> R.drawable.icn_notif_all_messages
NotificationType.MENTIONS_ONLY -> R.drawable.icn_notif_mention_only
NotificationType.NONE -> R.drawable.icn_notif_none
}
),
contentDescription = null
)
},
trailingContent = {
RadioButton(
selected = isSelected,
onClick = { updateSelection(type) }
)
},
onClick = { updateSelection(type) }
)
}
ServerNotificationRow(
title = stringResource(id = R.string.notification_option_use_default),
type = NotificationType.DEFAULT
)
HorizontalDivider(modifier = Modifier.padding(horizontal = 12.dp))
ServerNotificationRow(
title = stringResource(id = R.string.notification_option_muted),
type = NotificationType.MUTED
)
HorizontalDivider(modifier = Modifier.padding(horizontal = 12.dp))
ServerNotificationRow(
title = stringResource(id = R.string.notification_option_all_messages),
type = NotificationType.ALL
)
HorizontalDivider(modifier = Modifier.padding(horizontal = 12.dp))
ServerNotificationRow(
title = stringResource(id = R.string.notification_option_mentions_only),
type = NotificationType.MENTIONS_ONLY
)
HorizontalDivider(modifier = Modifier.padding(horizontal = 12.dp))
ServerNotificationRow(
title = stringResource(id = R.string.notification_option_none),
type = NotificationType.NONE
)
}
}
Spacer(modifier = Modifier.height(12.dp))
}
// Primary actions group
Column(
modifier = Modifier
.padding(horizontal = 12.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.surfaceBright)
) {
SheetButton(
leadingContent = {
Icon(
painter = painterResource(id = R.drawable.icn_identifier_copy_24dp),
contentDescription = null
)
},
headlineContent = {
Text(
text = stringResource(id = R.string.server_context_sheet_actions_copy_id)
)
},
onClick = {
if (server.id == null) return@SheetButton
clipboardManager.setText(AnnotatedString(server.id))
if (Platform.needsShowClipboardNotification()) {
Toast.makeText(
context,
context.getString(R.string.server_context_sheet_actions_copy_id_copied),
Toast.LENGTH_SHORT
).show()
}
coroutineScope.launch {
onHideSheet()
}
}
)
HorizontalDivider()
SheetButton(
leadingContent = {
Icon(
painter = painterResource(id = R.drawable.icn_mark_chat_read_24dp),
contentDescription = null
)
},
headlineContent = {
Text(
text = stringResource(id = R.string.server_context_sheet_actions_mark_read)
)
},
onClick = {
coroutineScope.launch {
server.id?.let {
PeptideAPI.unreads.markServerAsRead(it, sync = true)
}
onHideSheet()
}
}
)
}
// Danger actions group
if (server.owner != PeptideAPI.selfId) {
Column(
modifier = Modifier
.padding(start = 12.dp, end = 12.dp, top = 16.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.surfaceBright)
) {
SheetButton(
leadingContent = {
Icon(
painter = painterResource(id = R.drawable.icn_report_24dp),
contentDescription = null
)
},
headlineContent = {
Text(
text = stringResource(id = R.string.server_context_sheet_actions_report),
)
},
dangerous = true,
onClick = {
onReportServer()
}
)
HorizontalDivider()
SheetButton(
leadingContent = {
Icon(
painter = painterResource(id = R.drawable.icn_door_open_24dp),
contentDescription = null,
)
},
headlineContent = {
Text(
text = stringResource(id = R.string.server_context_sheet_actions_leave)
)
},
dangerous = true,
onClick = {
showLeaveConfirmation = true
}
)
}
}
}
}