diff --git a/app/src/main/java/chat/peptide/api/settings/NotificationType.kt b/app/src/main/java/chat/peptide/api/settings/NotificationType.kt
new file mode 100644
index 00000000..d203af6a
--- /dev/null
+++ b/app/src/main/java/chat/peptide/api/settings/NotificationType.kt
@@ -0,0 +1,25 @@
+package chat.peptide.api.settings
+
+enum class NotificationType(val storageValue: String) {
+ DEFAULT("default"),
+ MUTED("muted"),
+ ALL("all"),
+ MENTIONS_ONLY("mentions"),
+ NONE("none");
+
+ companion object {
+ fun fromStorage(value: String?): NotificationType {
+ return when (value) {
+ null -> DEFAULT
+ "default" -> DEFAULT
+ "muted" -> MUTED
+ "all" -> ALL
+ "mentions" -> MENTIONS_ONLY
+ "none" -> NONE
+ else -> DEFAULT
+ }
+ }
+ }
+}
+
+
diff --git a/app/src/main/java/chat/peptide/composables/generic/SheetButton.kt b/app/src/main/java/chat/peptide/composables/generic/SheetButton.kt
index cf8ea564..99b1eade 100644
--- a/app/src/main/java/chat/peptide/composables/generic/SheetButton.kt
+++ b/app/src/main/java/chat/peptide/composables/generic/SheetButton.kt
@@ -32,7 +32,6 @@ fun SheetButton(
Box(
modifier = modifier
.clickable { onClick() }
- .padding(horizontal = 16.dp)
) {
ListItem(
colors = TransparentListItemColours,
diff --git a/app/src/main/java/chat/peptide/sheets/ChannelContextSheet.kt b/app/src/main/java/chat/peptide/sheets/ChannelContextSheet.kt
index ca043c42..7b2f882f 100644
--- a/app/src/main/java/chat/peptide/sheets/ChannelContextSheet.kt
+++ b/app/src/main/java/chat/peptide/sheets/ChannelContextSheet.kt
@@ -1,28 +1,58 @@
package chat.peptide.sheets
import android.widget.Toast
+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.background
+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.fillMaxSize
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.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
import chat.peptide.R
import chat.peptide.api.PeptideAPI
+import chat.peptide.api.internals.ChannelUtils
+import chat.peptide.composables.generic.RemoteImage
import chat.peptide.composables.generic.SheetButton
-
import chat.peptide.internals.Platform
+import chat.peptide.screens.chat.dialogs.InviteDialog
import kotlinx.coroutines.launch
+import chat.peptide.api.settings.NotificationType
+import chat.peptide.api.settings.SyncedSettings
+import chat.peptide.api.schemas.NotificationSettings
@Composable
fun ChannelContextSheet(channelId: String, onHideSheet: suspend () -> Unit) {
@@ -38,15 +68,161 @@ fun ChannelContextSheet(channelId: String, onHideSheet: suspend () -> Unit) {
return
}
+ val server = channel.server?.let { PeptideAPI.serverCache[it] }
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
+ var inviteDialogShown by remember { mutableStateOf(false) }
+ if (inviteDialogShown) {
+ Dialog(onDismissRequest = { inviteDialogShown = false }) {
+ InviteDialog(
+ channelId = channelId,
+ onDismissRequest = { inviteDialogShown = false }
+ )
+ }
+ }
+
+ // Animated content pages inside the sheet
+ var currentPage by remember { mutableIntStateOf(0) } // 0 = main, 1 = notifications
+
+
+ // Action buttons or notifications page
+ AnimatedContent(
+ targetState = currentPage,
+ transitionSpec = {
+ if (targetState > initialState) {
+ // forward: slide in from right, out to left
+ slideInHorizontally(animationSpec = tween(200)) { fullWidth -> fullWidth } togetherWith
+ slideOutHorizontally(animationSpec = tween(200)) { fullWidth -> -fullWidth }
+ } else {
+ // back: slide in from left, out to right
+ slideInHorizontally(animationSpec = tween(200)) { fullWidth -> -fullWidth } togetherWith
+ slideOutHorizontally(animationSpec = tween(200)) { fullWidth -> fullWidth }
+ }
+ }, label = "Channel context sheet pager"
+ ) { page ->
+ if (page == 0) {
+ Column(Modifier.fillMaxWidth()) {
+ // Header with server image and channel info in a row layout
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 20.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ server?.icon?.id?.let { iconId ->
+ RemoteImage(
+ url = "${PeptideAPI.getCurrentFilesUrl()}/icons/$iconId",
+ allowAnimation = false,
+ modifier = Modifier
+ .size(40.dp)
+ .clip(RoundedCornerShape(8.dp)),
+ description = server.name ?: stringResource(R.string.unknown)
+ )
+ }
+
+ Text(
+ text = "# ${ChannelUtils.resolveName(channel) ?: stringResource(R.string.unknown)}",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ ) {
+ // Group 1: Mark as read + Notification options
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(16.dp))
+ .background(MaterialTheme.colorScheme.surfaceBright)
+ ) {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ SheetButton(
+ headlineContent = {
+ Text(
+ text = stringResource(id = R.string.channel_context_sheet_actions_mark_read),
+ fontWeight = FontWeight.Medium
+ )
+ },
+ leadingContent = {
+ Icon(
+ painter = painterResource(id = R.drawable.icn_mark_chat_read_24dp),
+ contentDescription = null
+ )
+ },
+ onClick = {
+ coroutineScope.launch {
+ channel.lastMessageID?.let {
+ PeptideAPI.unreads.markAsRead(channelId, it, sync = true)
+ }
+ onHideSheet()
+ }
+ }
+ )
+ HorizontalDivider(modifier = Modifier.padding(horizontal = 12.dp))
+ SheetButton(
+ headlineContent = {
+ Text(
+ text = stringResource(id = R.string.channel_info_sheet_options_notifications_manage),
+ fontWeight = FontWeight.Medium
+ )
+ },
+ leadingContent = {
+ Icon(
+ painter = painterResource(id = R.drawable.icn_notification_settings_24dp),
+ contentDescription = null
+ )
+ },
+ onClick = {
+ currentPage = 1
+ }
+ )
+ }
+ }
+ // Group 2: Invite + Copy channel ID
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 24.dp)
+ .clip(RoundedCornerShape(16.dp))
+ .background(MaterialTheme.colorScheme.surfaceBright)
+ ) {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ SheetButton(
+ headlineContent = {
+ Text(
+ text = stringResource(id = R.string.channel_info_sheet_options_invite),
+ fontWeight = FontWeight.Medium
+ )
+ },
+ leadingContent = {
+ Icon(
+ painter = painterResource(id = R.drawable.icn_add_24dp),
+ contentDescription = null
+ )
+ },
+ onClick = {
+ inviteDialogShown = true
+ }
+ )
+ HorizontalDivider(modifier = Modifier.padding(horizontal = 12.dp))
SheetButton(
headlineContent = {
Text(
text = stringResource(id = R.string.channel_context_sheet_actions_copy_id),
+ fontWeight = FontWeight.Medium
)
},
leadingContent = {
@@ -73,28 +249,180 @@ fun ChannelContextSheet(channelId: String, onHideSheet: suspend () -> Unit) {
}
}
)
+ }
+ }
+ }
+ }
+ }else{
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ ) {
+ // Top bar for notifications page
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_arrow_left_24dp),
+ contentDescription = null,
+ modifier = Modifier
+ .size(24.dp)
+ .clickable { currentPage = 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,
+ fontWeight = FontWeight.SemiBold
+ )
+ }
+ // Notifications controls container (with radio options)
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 24.dp)
+ .clip(RoundedCornerShape(16.dp))
+ .background(MaterialTheme.colorScheme.surfaceBright)
+ ) {
+ // Load current selection from SyncedSettings
+ var selected by remember {
+ mutableStateOf(
+ NotificationType.fromStorage(
+ SyncedSettings.notifications.channel[channelId]
+ ).storageValue
+ )
+ }
+ Column(modifier = Modifier.fillMaxWidth()) {
+ NotificationRadioRow(
+ title = stringResource(id = R.string.notification_option_use_default),
+ type = NotificationType.DEFAULT,
+ channelId = channelId,
+ current = selected,
+ onChange = { selected = it }
+ )
+ HorizontalDivider(modifier = Modifier.padding(horizontal = 12.dp))
+ NotificationRadioRow(
+ title = stringResource(id = R.string.notification_option_muted),
+ type = NotificationType.MUTED,
+ channelId = channelId,
+ current = selected,
+ onChange = { selected = it }
+ )
+ HorizontalDivider(modifier = Modifier.padding(horizontal = 12.dp))
+ NotificationRadioRow(
+ title = stringResource(id = R.string.notification_option_all_messages),
+ type = NotificationType.ALL,
+ channelId = channelId,
+ current = selected,
+ onChange = { selected = it }
+ )
+ HorizontalDivider(modifier = Modifier.padding(horizontal = 12.dp))
+ NotificationRadioRow(
+ title = stringResource(id = R.string.notification_option_mentions_only),
+ type = NotificationType.MENTIONS_ONLY,
+ channelId = channelId,
+ current = selected,
+ onChange = { selected = it }
+ )
+ HorizontalDivider(modifier = Modifier.padding(horizontal = 12.dp))
+ NotificationRadioRow(
+ title = stringResource(id = R.string.notification_option_none),
+ type = NotificationType.NONE,
+ channelId = channelId,
+ current = selected,
+ onChange = { selected = it }
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun NotificationOptionButton(
+ title: String,
+ isSelected: Boolean,
+ onSelect: () -> Unit
+) {
SheetButton(
- headlineContent = {
- Text(
- text = stringResource(id = R.string.channel_context_sheet_actions_mark_read),
- )
- },
+ headlineContent = { Text(text = title) },
leadingContent = {
Icon(
- painter = painterResource(id = R.drawable.icn_mark_chat_read_24dp),
+ painter = painterResource(id = R.drawable.icn_notification_settings_24dp),
contentDescription = null
)
},
- onClick = {
- coroutineScope.launch {
- channel.lastMessageID?.let {
- PeptideAPI.unreads.markAsRead(channelId, it, sync = true)
+ trailingContent = {
+ androidx.compose.material3.RadioButton(
+ selected = isSelected,
+ onClick = onSelect
+ )
+ },
+ onClick = onSelect
+ )
+}
+
+@Composable
+private fun NotificationRadioRow(
+ title: String,
+ type: NotificationType,
+ channelId: String,
+ current: String,
+ onChange: (String) -> Unit
+) {
+ val isSelected = current == type.storageValue
+ SheetButton(
+ headlineContent = { Text(text = title) },
+ leadingContent = {
+ Icon(
+ painter = painterResource(id = R.drawable.icn_notification_settings_24dp),
+ contentDescription = null
+ )
+ },
+ trailingContent = {
+ androidx.compose.material3.RadioButton(
+ selected = isSelected,
+ onClick = {
+ onChange(type.storageValue)
+ val currentSettings = SyncedSettings.notifications
+ val updated = NotificationSettings(
+ server = currentSettings.server,
+ channel = currentSettings.channel.toMutableMap().apply {
+ if (type == NotificationType.DEFAULT) {
+ remove(channelId)
+ } else {
+ put(channelId, type.storageValue)
+ }
+ }
+ )
+ kotlinx.coroutines.GlobalScope.launch {
+ SyncedSettings.updateNotifications(updated)
+ }
}
- onHideSheet()
+ )
+ },
+ onClick = {
+ onChange(type.storageValue)
+ val currentSettings = SyncedSettings.notifications
+ val updated = NotificationSettings(
+ server = currentSettings.server,
+ channel = currentSettings.channel.toMutableMap().apply {
+ if (type == NotificationType.DEFAULT) {
+ remove(channelId)
+ } else {
+ put(channelId, type.storageValue)
+ }
+ }
+ )
+ kotlinx.coroutines.GlobalScope.launch {
+ SyncedSettings.updateNotifications(updated)
}
}
)
-
-
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 07facf1c..d6da0a62 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -660,6 +660,13 @@
We\'re thrilled to have you on board. Let\'s make something great together!
Got it
+
+ Use default
+ Muted
+ All messages
+ Mentions only
+ None
+
Swipe to Reply is here.
Making message threads has never been easier.\nJust swipe to the left on any message.\nReply up to five messages at once.
Got it