revert(meetingrooms): drop the kind:30313 drawer split

User changed direction — the Nests / Meeting Rooms separation was
the wrong call. Kind 30313 stays out of the Nests list, but doesn't
get its own drawer entry either; it surfaces through the standard
NoteCompose / thread paths instead.

Reverted:
  - Deleted: meetingrooms/MeetingRoomsScreen, MeetingRoomsTopBar,
    MeetingRoomsFeedLoaded, dal/MeetingRoomsFeedFilter.
  - Removed Route.MeetingRooms, NavBarItem.MEETING_ROOMS, the
    catalog entry, the drawer entry.
  - Removed meetingRoomsFeed from AccountFeedContentStates and
    ScrollStateKeys.MEETING_ROOMS_SCREEN.
  - Removed `meeting_rooms` string.
  - Updated AppNavigation imports.

Kept:
  - NestsFeedFilter narrowed to kind:30312 only (per user: "only
    show kind 30312 in the items of the NestScreen"). The dual-kind
    branches stay gone.
  - Doc comment updated to note that kind:30313 renders on the
    standard NoteCompose / thread paths outside the Nests drawer.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
This commit is contained in:
Claude
2026-04-27 19:07:44 +00:00
parent befa13d797
commit 0ceeafe90c
11 changed files with 6 additions and 462 deletions
@@ -68,7 +68,6 @@ object ScrollStateKeys {
const val FOLLOW_PACKS_SCREEN = "FollowPacksFeed"
const val LIVE_STREAMS_SCREEN = "LiveStreamsFeed"
const val NESTS_SCREEN = "NestsFeed"
const val MEETING_ROOMS_SCREEN = "MeetingRoomsFeed"
const val LONGS_SCREEN = "LongsFeed"
const val ARTICLES_SCREEN = "ArticlesFeed"
@@ -127,7 +127,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.PeopleL
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.memberEdit.FollowListAndPackAndUserScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.LiveStreamsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.LongsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.meetingrooms.MeetingRoomsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.NestsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser.ImportFollowListPickFollowsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser.ImportFollowListSelectUserScreen
@@ -247,7 +246,6 @@ fun BuildNavigation(
composableFromEnd<Route.FollowPacks> { FollowPacksScreen(accountViewModel, nav) }
composableFromEnd<Route.LiveStreams> { LiveStreamsScreen(accountViewModel, nav) }
composableFromEnd<Route.Nests> { NestsScreen(accountViewModel, nav) }
composableFromEnd<Route.MeetingRooms> { MeetingRoomsScreen(accountViewModel, nav) }
composableFromEnd<Route.Longs> { LongsScreen(accountViewModel, nav) }
composableFromEnd<Route.Articles> { ArticlesScreen(accountViewModel, nav) }
composableFromEnd<Route.NewHlsVideo> { NewHlsVideoScreen(accountViewModel, nav) }
@@ -55,7 +55,6 @@ enum class NavBarItem {
FOLLOW_PACKS,
LIVE_STREAMS,
NESTS,
MEETING_ROOMS,
LONGS,
POLLS,
BADGES,
@@ -220,13 +219,6 @@ val NavBarCatalog: Map<NavBarItem, NavBarItemDef> =
icon = MaterialSymbols.Mic,
resolveRoute = { Route.Nests },
),
NavBarItem.MEETING_ROOMS to
NavBarItemDef(
id = NavBarItem.MEETING_ROOMS,
labelRes = R.string.meeting_rooms,
icon = MaterialSymbols.Groups,
resolveRoute = { Route.MeetingRooms },
),
NavBarItem.LONGS to
NavBarItemDef(
id = NavBarItem.LONGS,
@@ -314,7 +306,6 @@ val DrawerFeedsItems: List<NavBarItem> =
NavBarItem.FOLLOW_PACKS,
NavBarItem.LIVE_STREAMS,
if (isDebug) NavBarItem.NESTS else null,
if (isDebug) NavBarItem.MEETING_ROOMS else null,
NavBarItem.LONGS,
NavBarItem.POLLS,
NavBarItem.BADGES,
@@ -91,8 +91,6 @@ sealed class Route {
@Serializable object Nests : Route()
@Serializable object MeetingRooms : Route()
@Serializable object Longs : Route()
@Serializable object Articles : Route()
@@ -47,7 +47,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeLiveFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeNewThreadFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.dal.LiveStreamsFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.dal.LongsFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.meetingrooms.dal.MeetingRoomsFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.dal.NestsFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedContentState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationSummaryState
@@ -103,7 +102,6 @@ class AccountFeedContentStates(
val followPacksFeed = FeedContentState(FollowPacksFeedFilter(account), scope, LocalCache)
val liveStreamsFeed = FeedContentState(LiveStreamsFeedFilter(account), scope, LocalCache)
val nestsFeed = FeedContentState(NestsFeedFilter(account), scope, LocalCache)
val meetingRoomsFeed = FeedContentState(MeetingRoomsFeedFilter(account), scope, LocalCache)
val longsFeed = FeedContentState(LongsFeedFilter(account), scope, LocalCache)
val articlesFeed = FeedContentState(ArticlesFeedFilter(account), scope, LocalCache)
@@ -171,7 +169,6 @@ class AccountFeedContentStates(
followPacksFeed.updateFeedWith(newNotes)
liveStreamsFeed.updateFeedWith(newNotes)
nestsFeed.updateFeedWith(newNotes)
meetingRoomsFeed.updateFeedWith(newNotes)
longsFeed.updateFeedWith(newNotes)
articlesFeed.updateFeedWith(newNotes)
@@ -219,7 +216,6 @@ class AccountFeedContentStates(
followPacksFeed.deleteFromFeed(newNotes)
liveStreamsFeed.deleteFromFeed(newNotes)
nestsFeed.deleteFromFeed(newNotes)
meetingRoomsFeed.deleteFromFeed(newNotes)
longsFeed.deleteFromFeed(newNotes)
articlesFeed.deleteFromFeed(newNotes)
@@ -1,107 +0,0 @@
/*
* 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.meetingrooms
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.RenderLiveActivityThumb
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdPadding
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent
/**
* Read-only list of NIP-53 kind 30313 [MeetingRoomEvent]s. Each row
* mirrors the Discovery card UI ([RenderLiveActivityThumb]) — cover
* image with status flag, participant gallery overlay, and the
* channel header (creator, name, like + zap reactions) at the
* bottom. Tapping a row opens the standard thread view via
* `routeFor` so the user can see the meeting's full details and
* any `streaming` URL.
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MeetingRoomsFeedLoaded(
loaded: FeedState.Loaded,
listState: LazyListState,
accountViewModel: AccountViewModel,
nav: INav,
) {
val items by loaded.feed.collectAsStateWithLifecycle()
LazyColumn(
contentPadding = rememberFeedContentPadding(FeedPadding),
state = listState,
) {
itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item ->
Row(Modifier.fillMaxWidth().animateItem()) {
MeetingRoomFeedCard(
baseNote = item,
modifier = Modifier.fillMaxWidth(),
accountViewModel = accountViewModel,
nav = nav,
)
}
HorizontalDivider(
thickness = DividerThickness,
)
}
}
}
@Composable
private fun MeetingRoomFeedCard(
baseNote: Note,
modifier: Modifier,
accountViewModel: AccountViewModel,
nav: INav,
) {
if (baseNote.event !is MeetingRoomEvent) return
Column(
modifier =
modifier.clickable {
nav.nav { routeFor(baseNote, accountViewModel.account) }
},
) {
Column(StdPadding) {
RenderLiveActivityThumb(baseNote, accountViewModel, nav)
}
}
}
@@ -1,130 +0,0 @@
/*
* 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.meetingrooms
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
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.amethyst.ui.screen.loggedIn.nests.datasource.NestsFilterAssemblerSubscription
/**
* Drawer screen for NIP-53 kind 30313 (Meeting). Reads from the
* `meetingRoomsFeed` content state which scans
* `LocalCache.liveChatChannels` for `MeetingRoomEvent`s; the wire
* subscription is the same `makeLiveActivitiesFilter` the Nests
* screen uses (it requests kinds 30311/30312/30313/1311 in one
* REQ), so mounting the `NestsFilterAssemblerSubscription` here
* keeps wire traffic identical whichever screen the user lands
* on first.
*
* Read-only list — the app doesn't compose kind-30313 events
* locally. The per-row CTA (Join, recording link, etc.) lives
* inside [com.vitorpamplona.amethyst.ui.note.types.RenderMeetingRoomEvent].
*/
@Composable
fun MeetingRoomsScreen(
accountViewModel: AccountViewModel,
nav: INav,
) {
MeetingRoomsScreen(
meetingRoomsFeedContentState = accountViewModel.feedStates.meetingRoomsFeed,
accountViewModel = accountViewModel,
nav = nav,
)
}
@Composable
fun MeetingRoomsScreen(
meetingRoomsFeedContentState: FeedContentState,
accountViewModel: AccountViewModel,
nav: INav,
) {
WatchLifecycleAndUpdateModel(meetingRoomsFeedContentState)
WatchAccountForMeetingRoomsScreen(meetingRoomsFeedState = meetingRoomsFeedContentState, accountViewModel = accountViewModel)
NestsFilterAssemblerSubscription(accountViewModel)
DisappearingScaffold(
isInvertedLayout = false,
topBar = {
MeetingRoomsTopBar(accountViewModel, nav)
},
bottomBar = {
AppBottomBar(Route.MeetingRooms, accountViewModel) { route ->
if (route == Route.MeetingRooms) {
meetingRoomsFeedContentState.sendToTop()
} else {
nav.navBottomBar(route)
}
}
},
accountViewModel = accountViewModel,
) {
RefresheableBox(meetingRoomsFeedContentState, true) {
SaveableFeedContentState(meetingRoomsFeedContentState, scrollStateKey = ScrollStateKeys.MEETING_ROOMS_SCREEN) { listState ->
RenderFeedContentState(
feedContentState = meetingRoomsFeedContentState,
accountViewModel = accountViewModel,
listState = listState,
nav = nav,
routeForLastRead = "MeetingRoomsFeed",
onLoaded = { loaded ->
MeetingRoomsFeedLoaded(
loaded = loaded,
listState = listState,
accountViewModel = accountViewModel,
nav = nav,
)
},
)
}
}
}
}
@Composable
fun WatchAccountForMeetingRoomsScreen(
meetingRoomsFeedState: FeedContentState,
accountViewModel: AccountViewModel,
) {
val listState by accountViewModel.account.liveLiveStreamsFollowLists.collectAsStateWithLifecycle()
// Use `by` to unwrap the StateFlow's value into the LaunchedEffect
// key — without it, the State<T> object is reference-stable across
// recompositions and the effect never re-fires when the user
// mutes someone, leaving the feed stale until a manual refresh.
val hiddenUsers by accountViewModel.account.hiddenUsers.flow
.collectAsStateWithLifecycle()
LaunchedEffect(accountViewModel, listState, hiddenUsers) {
meetingRoomsFeedState.checkKeysInvalidateDataAndSendToTop()
}
}
@@ -1,70 +0,0 @@
/*
* 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.meetingrooms
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.TopFilter
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.FeedFilterSpinner
import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@Composable
fun MeetingRoomsTopBar(
accountViewModel: AccountViewModel,
nav: INav,
) {
UserDrawerSearchTopBar(accountViewModel, nav) {
val list by accountViewModel.account.settings.defaultLiveStreamsFollowList
.collectAsStateWithLifecycle()
MeetingRoomsTopNavFilterBar(
followListsModel = accountViewModel.feedStates.feedListOptions,
listName = list,
accountViewModel = accountViewModel,
onChange = accountViewModel.account.settings::changeDefaultLiveStreamsFollowList,
)
}
}
@Composable
private fun MeetingRoomsTopNavFilterBar(
followListsModel: TopNavFilterState,
listName: TopFilter,
accountViewModel: AccountViewModel,
onChange: (FeedDefinition) -> Unit,
) {
val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsStateWithLifecycle()
FeedFilterSpinner(
placeholderCode = listName,
explainer = stringRes(R.string.select_list_to_filter),
options = allLists,
onSelect = onChange,
accountViewModel = accountViewModel,
)
}
@@ -1,132 +0,0 @@
/*
* 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.meetingrooms.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.amethyst.model.TopFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByProxyTopNavFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag
/**
* Drawer feed for NIP-53 kind 30313 (Meeting). Pulls
* [MeetingRoomEvent]s out of `LocalCache.liveChatChannels` — which is
* the same backing store the Live Streams and Nests feeds walk —
* and presents them in their own drawer entry so the audio-rooms
* surface (kind 30312) doesn't mix scheduled meetings into its list.
*
* Required-field gate: a kind-30313 with a blank title is
* unrenderable, so it's skipped at the feed level — same UX rule
* the Nests feed applies to its own missing-fields case.
*/
class MeetingRoomsFeedFilter(
val account: Account,
) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList().code
override fun limit() = 50
fun followList(): TopFilter = account.settings.defaultLiveStreamsFollowList.value
private fun TopFilter.isMuteList() = this is TopFilter.MuteList
private fun TopFilter.isBlockList() = this is TopFilter.PeopleList && this.address == account.blockPeopleList.getBlockListAddress()
private fun TopFilter.wantsToSeeNegativeStuff() = isMuteList() || isBlockList()
override fun showHiddenKey(): Boolean = followList().wantsToSeeNegativeStuff()
override fun feed(): List<Note> {
val allNotes = LocalCache.liveChatChannels.mapNotNull { _, channel -> LocalCache.getAddressableNoteIfExists(channel.address) }
return sort(innerApplyFilter(allNotes))
}
override fun applyFilter(newItems: Set<Note>): Set<Note> = innerApplyFilter(newItems)
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val filterParams =
FilterByListParams.create(
followLists = account.liveLiveStreamsFollowLists.value,
hiddenUsers = account.hiddenUsers.flow.value,
)
return collection.filterTo(HashSet()) {
val noteEvent = it.event as? MeetingRoomEvent ?: return@filterTo false
!noteEvent.title().isNullOrBlank() && filterParams.match(noteEvent, it.relays)
}
}
override fun sort(items: Set<Note>): List<Note> {
val topFilter = account.liveLiveStreamsFollowLists.value
val topFilterAuthors =
when (topFilter) {
is AuthorsByOutboxTopNavFilter -> topFilter.authors
is MutedAuthorsByOutboxTopNavFilter -> topFilter.authors
is AllFollowsByOutboxTopNavFilter -> topFilter.authors
is SingleCommunityTopNavFilter -> topFilter.authors
is AuthorsByProxyTopNavFilter -> topFilter.authors
is MutedAuthorsByProxyTopNavFilter -> topFilter.authors
is AllFollowsByProxyTopNavFilter -> topFilter.authors
else -> null
}
val followingKeySet = topFilterAuthors ?: account.kind3FollowList.flow.value.authors
val counter = ParticipantListBuilder()
val participantCounts = items.associate { it to counter.countFollowsThatParticipateOn(it, followingKeySet) }
val allParticipants = items.associate { it to counter.countFollowsThatParticipateOn(it, null) }
return items
.sortedWith(
compareBy(
{ convertStatusToOrder(it.event as? MeetingRoomEvent) },
{ participantCounts[it] },
{ allParticipants[it] },
{
when (val e = it.event) {
is MeetingRoomEvent -> e.starts() ?: it.createdAt()
else -> it.createdAt()
}
},
{ it.idHex },
),
).reversed()
}
private fun convertStatusToOrder(event: MeetingRoomEvent?): Int =
when (event?.status()) {
StatusTag.STATUS.LIVE -> 2
StatusTag.STATUS.PLANNED -> 1
StatusTag.STATUS.ENDED -> 0
else -> 0
}
}
@@ -40,10 +40,12 @@ import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag
/**
* Drawer feed for NIP-53 kind 30312 (Interactive Rooms / audio spaces).
*
* Shares LocalCache.liveChatChannels with the Live Streams feed, narrowed
* to MeetingSpaceEvent (30312) so the Clubhouse-style audio-room surface
* is independent of video live streams. Kind 30313 (NIP-53 meetings) lives
* in a sibling drawer entry — see [com.vitorpamplona.amethyst.ui.screen.loggedIn.meetingrooms.dal.MeetingRoomsFeedFilter].
* Shares LocalCache.liveChatChannels with the Live Streams feed,
* narrowed to MeetingSpaceEvent (30312) so the audio-room surface
* is independent of video live streams. Kind 30313 (NIP-53 meetings)
* intentionally does NOT surface here — the room concept and the
* meeting concept render on the standard NoteCompose / thread paths
* outside the Nests drawer.
*/
class NestsFeedFilter(
val account: Account,
-1
View File
@@ -489,7 +489,6 @@
<string name="follow_packs">Follow Packs</string>
<string name="live_streams">Live Streams</string>
<string name="nests">Nests</string>
<string name="meeting_rooms">Meeting Rooms</string>
<string name="nest_stage">Stage</string>
<string name="nest_audience">Audience</string>
<string name="nest_raise_hand">Raise hand</string>