mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 00:16:59 +00:00
feat(nip29): grouped view mode + view-mode toggle & persistence (C, part 4)
Completes the Messages-tab integration and makes the view mode user-switchable. - RelayGroupServerList: the GROUPED mode's relay rows (one per host relay of the user's joined groups), tap opens the relay's channel list; rendered above the DM feed in MessagesSinglePane only in GROUPED mode (MessagesPager gains a modifier param so it weights correctly under the section). - RelayGroupViewModeToggle: a segmented Inline/By-relay control, shown once the user has joined a group; flips AccountSettings.relayGroupViewMode. - Persistence: the view mode is saved/restored via LocalPreferences (mirrors defaultRelayAuthPolicy), with an updateRelayGroupViewMode setter that saves. The full NIP-29 relay-groups feature is now navigable end-to-end: browse a relay's channels, chat in a group, and see joined groups in the Messages tab in either inline (default) or grouped view. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
This commit is contained in:
@@ -26,6 +26,7 @@ import android.content.SharedPreferences
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.core.content.edit
|
||||
import com.vitorpamplona.amethyst.commons.model.clink.ClinkDebitWalletEntry
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupViewMode
|
||||
import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.NwcWalletEntry
|
||||
import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.NwcWalletEntryNorm
|
||||
import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthPolicy
|
||||
@@ -159,6 +160,7 @@ private object PrefKeys {
|
||||
const val HIDE_NIP_17_WARNING_DIALOG = "hide_nip24_warning_dialog" // delete later
|
||||
const val ALWAYS_ON_NOTIFICATION_SERVICE = "always_on_notification_service"
|
||||
const val DEFAULT_RELAY_AUTH_POLICY = "default_relay_auth_policy"
|
||||
const val RELAY_GROUP_VIEW_MODE = "relay_group_view_mode"
|
||||
const val SPLIT_NOTIFICATIONS_ENABLED = "split_notifications_enabled"
|
||||
const val SHOW_MESSAGES_IN_NOTIFICATIONS = "show_messages_in_notifications"
|
||||
|
||||
@@ -512,6 +514,7 @@ object LocalPreferences {
|
||||
putBoolean(PrefKeys.CALLS_ENABLED, settings.callsEnabled.value)
|
||||
putBoolean(PrefKeys.ALWAYS_ON_NOTIFICATION_SERVICE, settings.alwaysOnNotificationService.value)
|
||||
putString(PrefKeys.DEFAULT_RELAY_AUTH_POLICY, settings.defaultRelayAuthPolicy.value.name)
|
||||
putString(PrefKeys.RELAY_GROUP_VIEW_MODE, settings.relayGroupViewMode.value.name)
|
||||
putBoolean(PrefKeys.SPLIT_NOTIFICATIONS_ENABLED, settings.splitNotificationsEnabled.value)
|
||||
putBoolean(PrefKeys.SHOW_MESSAGES_IN_NOTIFICATIONS, settings.showMessagesInNotifications.value)
|
||||
// Any account that reaches a save has its notification filter in its
|
||||
@@ -632,6 +635,7 @@ object LocalPreferences {
|
||||
getString(PrefKeys.DEFAULT_RELAY_AUTH_POLICY, null)
|
||||
?.let { runCatching { RelayAuthPolicy.valueOf(it) }.getOrNull() }
|
||||
?: RelayAuthPolicy.IF_IN_MY_LIST
|
||||
val relayGroupViewMode = RelayGroupViewMode.fromName(getString(PrefKeys.RELAY_GROUP_VIEW_MODE, null))
|
||||
val splitNotificationsEnabled = getBoolean(PrefKeys.SPLIT_NOTIFICATIONS_ENABLED, false)
|
||||
val showMessagesInNotifications = getBoolean(PrefKeys.SHOW_MESSAGES_IN_NOTIFICATIONS, true)
|
||||
val hasDonatedInVersion = getStringSet(PrefKeys.HAS_DONATED_IN_VERSION, null) ?: setOf()
|
||||
@@ -839,6 +843,7 @@ object LocalPreferences {
|
||||
hideNIP17WarningDialog = hideNIP17WarningDialog,
|
||||
alwaysOnNotificationService = MutableStateFlow(alwaysOnNotificationService),
|
||||
defaultRelayAuthPolicy = MutableStateFlow(defaultRelayAuthPolicy),
|
||||
relayGroupViewMode = MutableStateFlow(relayGroupViewMode),
|
||||
splitNotificationsEnabled = MutableStateFlow(splitNotificationsEnabled),
|
||||
showMessagesInNotifications = MutableStateFlow(showMessagesInNotifications),
|
||||
backupUserMetadata = latestUserMetadataResolved,
|
||||
|
||||
@@ -290,6 +290,13 @@ class AccountSettings(
|
||||
|
||||
fun isWriteable(): Boolean = keyPair.privKey != null || externalSignerPackageName != null
|
||||
|
||||
fun updateRelayGroupViewMode(mode: RelayGroupViewMode) {
|
||||
if (relayGroupViewMode.value != mode) {
|
||||
relayGroupViewMode.tryEmit(mode)
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// Always-on Notification Service
|
||||
// ---
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
||||
|
||||
/**
|
||||
* The "grouped" Messages view: one row per host relay of the user's joined NIP-29
|
||||
* groups. Tapping a relay opens its channel list. Rendered above the DM feed only
|
||||
* in [com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupViewMode.GROUPED].
|
||||
*/
|
||||
@Composable
|
||||
fun RelayGroupServerList(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val servers by accountViewModel.account.relayGroupList.liveRelayGroupServers
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
if (servers.isEmpty()) return
|
||||
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
servers.sorted().forEach { server ->
|
||||
RelayServerRow(server) { nav.nav(Route.RelayGroupServer(server)) }
|
||||
HorizontalDivider(thickness = 0.25.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RelayServerRow(
|
||||
relayUrl: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val label = RelayUrlNormalizer.normalizeOrNull(relayUrl)?.displayUrl() ?: relayUrl
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Text(
|
||||
text = "›",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupViewMode
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
/**
|
||||
* A compact toggle to switch how the user's NIP-29 groups appear in the Messages
|
||||
* tab — [RelayGroupViewMode.INLINE] (channels as rows with a relay chip) vs
|
||||
* [RelayGroupViewMode.GROUPED] (a row per relay, drill in for its channels).
|
||||
* Only shown once the user has joined at least one group.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RelayGroupViewModeToggle(accountViewModel: AccountViewModel) {
|
||||
val groups by accountViewModel.account.relayGroupList.liveRelayGroupList
|
||||
.collectAsStateWithLifecycle()
|
||||
if (groups.isEmpty()) return
|
||||
|
||||
val mode by accountViewModel.account.settings.relayGroupViewMode
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
SingleChoiceSegmentedButtonRow(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 6.dp),
|
||||
) {
|
||||
SegmentedButton(
|
||||
selected = mode == RelayGroupViewMode.INLINE,
|
||||
onClick = { accountViewModel.account.settings.updateRelayGroupViewMode(RelayGroupViewMode.INLINE) },
|
||||
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2),
|
||||
) {
|
||||
Text(stringRes(R.string.relay_group_view_inline))
|
||||
}
|
||||
SegmentedButton(
|
||||
selected = mode == RelayGroupViewMode.GROUPED,
|
||||
onClick = { accountViewModel.account.settings.updateRelayGroupViewMode(RelayGroupViewMode.GROUPED) },
|
||||
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2),
|
||||
) {
|
||||
Text(stringRes(R.string.relay_group_view_grouped))
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -119,12 +119,13 @@ fun MessagesPager(
|
||||
tabs: List<MessagesTabItem>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
userScrollEnabled = true,
|
||||
modifier =
|
||||
Modifier.zonedDrawerSwipe(
|
||||
modifier.zonedDrawerSwipe(
|
||||
pagerState = pagerState,
|
||||
openDrawer = nav::openDrawer,
|
||||
),
|
||||
|
||||
+21
-6
@@ -26,7 +26,10 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupViewMode
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
@@ -38,6 +41,8 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.AmethystClickableIcon
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupServerList
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupViewModeToggle
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelFabColumn
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.feed.MessagesPager
|
||||
@@ -98,11 +103,21 @@ fun MessagesSinglePane(
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
MessagesPager(
|
||||
pagerState,
|
||||
tabs,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
val viewMode by accountViewModel.account.settings.relayGroupViewMode
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
Column {
|
||||
RelayGroupViewModeToggle(accountViewModel)
|
||||
if (viewMode == RelayGroupViewMode.GROUPED) {
|
||||
RelayGroupServerList(accountViewModel, nav)
|
||||
}
|
||||
MessagesPager(
|
||||
pagerState,
|
||||
tabs,
|
||||
accountViewModel,
|
||||
nav,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1929,6 +1929,8 @@
|
||||
<string name="messages_create_public_chat">Public</string>
|
||||
<string name="messages_create_group">Group</string>
|
||||
<string name="messages_create_public_private_chat_description">New Public or Private Group</string>
|
||||
<string name="relay_group_view_inline">Inline</string>
|
||||
<string name="relay_group_view_grouped">By relay</string>
|
||||
<string name="messages_new_message">Private</string>
|
||||
<string name="messages_new_message_to">To</string>
|
||||
<string name="messages_new_message_subject">Subject</string>
|
||||
|
||||
Reference in New Issue
Block a user