feat: Enhance ChannelContextSheet with notification settings and invite functionality
This commit introduces a notifications page within the ChannelContextSheet, allowing users to manage notification preferences for channels. It also adds an invite dialog for inviting users to the channel. - Implemented animated content transitions between the main actions and notifications page. - Added notification options for default, muted, all messages, mentions only, and none. - Created a reusable NotificationRadioRow component for managing notification settings. - Integrated an invite dialog that can be triggered from the channel context sheet. - Updated string resources to include labels for notification options.
This commit is contained in:
parent
69a68788f8
commit
bd4fd09559
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -32,7 +32,6 @@ fun SheetButton(
|
|||
Box(
|
||||
modifier = modifier
|
||||
.clickable { onClick() }
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
ListItem(
|
||||
colors = TransparentListItemColours,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -660,6 +660,13 @@
|
|||
<string name="spark_early_access_description_3">We\'re thrilled to have you on board. Let\'s make something great together!</string>
|
||||
<string name="spark_early_access_cta">Got it</string>
|
||||
|
||||
<!-- Notifications page labels -->
|
||||
<string name="notification_option_use_default">Use default</string>
|
||||
<string name="notification_option_muted">Muted</string>
|
||||
<string name="notification_option_all_messages">All messages</string>
|
||||
<string name="notification_option_mentions_only">Mentions only</string>
|
||||
<string name="notification_option_none">None</string>
|
||||
|
||||
<string name="spark_swipe_to_reply">Swipe to Reply is here.</string>
|
||||
<string name="spark_swipe_to_reply_description">Making message threads has never been easier.\nJust swipe to the left on any message.\nReply up to five messages at once.</string>
|
||||
<string name="spark_swipe_to_reply_cta">Got it</string>
|
||||
|
|
|
|||
Loading…
Reference in New Issue