Merge remote-tracking branch 'origin/main' into claude/configurable-bottom-nav-rFdQw
This commit is contained in:
@@ -103,6 +103,8 @@ private object PrefKeys {
|
|||||||
const val DEFAULT_PICTURES_FOLLOW_LIST = "defaultPicturesFollowList"
|
const val DEFAULT_PICTURES_FOLLOW_LIST = "defaultPicturesFollowList"
|
||||||
const val DEFAULT_PRODUCTS_FOLLOW_LIST = "defaultProductsFollowList"
|
const val DEFAULT_PRODUCTS_FOLLOW_LIST = "defaultProductsFollowList"
|
||||||
const val DEFAULT_SHORTS_FOLLOW_LIST = "defaultShortsFollowList"
|
const val DEFAULT_SHORTS_FOLLOW_LIST = "defaultShortsFollowList"
|
||||||
|
const val DEFAULT_PUBLIC_CHATS_FOLLOW_LIST = "defaultPublicChatsFollowList"
|
||||||
|
const val DEFAULT_LIVE_STREAMS_FOLLOW_LIST = "defaultLiveStreamsFollowList"
|
||||||
const val DEFAULT_LONGS_FOLLOW_LIST = "defaultLongsFollowList"
|
const val DEFAULT_LONGS_FOLLOW_LIST = "defaultLongsFollowList"
|
||||||
const val DEFAULT_ARTICLES_FOLLOW_LIST = "defaultArticlesFollowList"
|
const val DEFAULT_ARTICLES_FOLLOW_LIST = "defaultArticlesFollowList"
|
||||||
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer" // legacy, kept for migration
|
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer" // legacy, kept for migration
|
||||||
@@ -348,6 +350,8 @@ object LocalPreferences {
|
|||||||
putString(PrefKeys.DEFAULT_PICTURES_FOLLOW_LIST, JsonMapper.toJson(settings.defaultPicturesFollowList.value))
|
putString(PrefKeys.DEFAULT_PICTURES_FOLLOW_LIST, JsonMapper.toJson(settings.defaultPicturesFollowList.value))
|
||||||
putString(PrefKeys.DEFAULT_PRODUCTS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultProductsFollowList.value))
|
putString(PrefKeys.DEFAULT_PRODUCTS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultProductsFollowList.value))
|
||||||
putString(PrefKeys.DEFAULT_SHORTS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultShortsFollowList.value))
|
putString(PrefKeys.DEFAULT_SHORTS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultShortsFollowList.value))
|
||||||
|
putString(PrefKeys.DEFAULT_PUBLIC_CHATS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultPublicChatsFollowList.value))
|
||||||
|
putString(PrefKeys.DEFAULT_LIVE_STREAMS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultLiveStreamsFollowList.value))
|
||||||
putString(PrefKeys.DEFAULT_LONGS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultLongsFollowList.value))
|
putString(PrefKeys.DEFAULT_LONGS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultLongsFollowList.value))
|
||||||
putString(PrefKeys.DEFAULT_ARTICLES_FOLLOW_LIST, JsonMapper.toJson(settings.defaultArticlesFollowList.value))
|
putString(PrefKeys.DEFAULT_ARTICLES_FOLLOW_LIST, JsonMapper.toJson(settings.defaultArticlesFollowList.value))
|
||||||
|
|
||||||
@@ -506,17 +510,7 @@ object LocalPreferences {
|
|||||||
val viewedPollResultNoteIdsStr = getString(PrefKeys.VIEWED_POLL_RESULT_NOTE_IDS, null)
|
val viewedPollResultNoteIdsStr = getString(PrefKeys.VIEWED_POLL_RESULT_NOTE_IDS, null)
|
||||||
val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf()
|
val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf()
|
||||||
|
|
||||||
val defaultHomeFollowListStr = getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null)
|
val followListPrefs = loadFollowListPrefs()
|
||||||
val defaultStoriesFollowListStr = getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null)
|
|
||||||
val defaultNotificationFollowListStr = getString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, null)
|
|
||||||
val defaultDiscoveryFollowListStr = getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null)
|
|
||||||
|
|
||||||
val defaultPollsFollowListStr = getString(PrefKeys.DEFAULT_POLLS_FOLLOW_LIST, null)
|
|
||||||
val defaultPicturesFollowListStr = getString(PrefKeys.DEFAULT_PICTURES_FOLLOW_LIST, null)
|
|
||||||
val defaultProductsFollowListStr = getString(PrefKeys.DEFAULT_PRODUCTS_FOLLOW_LIST, null)
|
|
||||||
val defaultShortsFollowListStr = getString(PrefKeys.DEFAULT_SHORTS_FOLLOW_LIST, null)
|
|
||||||
val defaultLongsFollowListStr = getString(PrefKeys.DEFAULT_LONGS_FOLLOW_LIST, null)
|
|
||||||
val defaultArticlesFollowListStr = getString(PrefKeys.DEFAULT_ARTICLES_FOLLOW_LIST, null)
|
|
||||||
|
|
||||||
val zapPaymentRequestServerStr = getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)
|
val zapPaymentRequestServerStr = getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)
|
||||||
val nwcWalletsStr = getString(PrefKeys.NWC_WALLETS, null)
|
val nwcWalletsStr = getString(PrefKeys.NWC_WALLETS, null)
|
||||||
@@ -547,18 +541,6 @@ object LocalPreferences {
|
|||||||
|
|
||||||
Log.d("LocalPreferences") { "Load account from file $npub - before parsing events" }
|
Log.d("LocalPreferences") { "Load account from file $npub - before parsing events" }
|
||||||
|
|
||||||
val defaultHomeFollowList = async { parseTopFilterOrDefault(defaultHomeFollowListStr, TopFilter.AllFollows) }
|
|
||||||
val defaultStoriesFollowList = async { parseTopFilterOrDefault(defaultStoriesFollowListStr, TopFilter.Global) }
|
|
||||||
val defaultNotificationFollowList = async { parseTopFilterOrDefault(defaultNotificationFollowListStr, TopFilter.Global) }
|
|
||||||
val defaultDiscoveryFollowList = async { parseTopFilterOrDefault(defaultDiscoveryFollowListStr, TopFilter.Global) }
|
|
||||||
|
|
||||||
val defaultPollsFollowList = async { parseTopFilterOrDefault(defaultPollsFollowListStr, TopFilter.Global) }
|
|
||||||
val defaultPicturesFollowList = async { parseTopFilterOrDefault(defaultPicturesFollowListStr, TopFilter.Global) }
|
|
||||||
val defaultProductsFollowList = async { parseTopFilterOrDefault(defaultProductsFollowListStr, TopFilter.AroundMe) }
|
|
||||||
val defaultShortsFollowList = async { parseTopFilterOrDefault(defaultShortsFollowListStr, TopFilter.Global) }
|
|
||||||
val defaultLongsFollowList = async { parseTopFilterOrDefault(defaultLongsFollowListStr, TopFilter.Global) }
|
|
||||||
val defaultArticlesFollowList = async { parseTopFilterOrDefault(defaultArticlesFollowListStr, TopFilter.AllFollows) }
|
|
||||||
|
|
||||||
val nwcWalletsLoaded =
|
val nwcWalletsLoaded =
|
||||||
async {
|
async {
|
||||||
val nwcWalletEntries = parseOrNull<List<NwcWalletEntry>>(nwcWalletsStr)
|
val nwcWalletEntries = parseOrNull<List<NwcWalletEntry>>(nwcWalletsStr)
|
||||||
@@ -625,16 +607,18 @@ object LocalPreferences {
|
|||||||
localRelayServers = MutableStateFlow(localRelayServers),
|
localRelayServers = MutableStateFlow(localRelayServers),
|
||||||
defaultFileServer = defaultFileServer.await(),
|
defaultFileServer = defaultFileServer.await(),
|
||||||
stripLocationOnUpload = stripLocationOnUpload,
|
stripLocationOnUpload = stripLocationOnUpload,
|
||||||
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList.await()),
|
defaultHomeFollowList = MutableStateFlow(followListPrefs.home),
|
||||||
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList.await()),
|
defaultStoriesFollowList = MutableStateFlow(followListPrefs.stories),
|
||||||
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList.await()),
|
defaultNotificationFollowList = MutableStateFlow(followListPrefs.notification),
|
||||||
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList.await()),
|
defaultDiscoveryFollowList = MutableStateFlow(followListPrefs.discovery),
|
||||||
defaultPollsFollowList = MutableStateFlow(defaultPollsFollowList.await()),
|
defaultPollsFollowList = MutableStateFlow(followListPrefs.polls),
|
||||||
defaultPicturesFollowList = MutableStateFlow(defaultPicturesFollowList.await()),
|
defaultPicturesFollowList = MutableStateFlow(followListPrefs.pictures),
|
||||||
defaultProductsFollowList = MutableStateFlow(defaultProductsFollowList.await()),
|
defaultProductsFollowList = MutableStateFlow(followListPrefs.products),
|
||||||
defaultShortsFollowList = MutableStateFlow(defaultShortsFollowList.await()),
|
defaultShortsFollowList = MutableStateFlow(followListPrefs.shorts),
|
||||||
defaultLongsFollowList = MutableStateFlow(defaultLongsFollowList.await()),
|
defaultPublicChatsFollowList = MutableStateFlow(followListPrefs.publicChats),
|
||||||
defaultArticlesFollowList = MutableStateFlow(defaultArticlesFollowList.await()),
|
defaultLiveStreamsFollowList = MutableStateFlow(followListPrefs.liveStreams),
|
||||||
|
defaultLongsFollowList = MutableStateFlow(followListPrefs.longs),
|
||||||
|
defaultArticlesFollowList = MutableStateFlow(followListPrefs.articles),
|
||||||
nwcWallets = MutableStateFlow(nwcWalletsLoaded.await().first),
|
nwcWallets = MutableStateFlow(nwcWalletsLoaded.await().first),
|
||||||
defaultNwcWalletId = MutableStateFlow(nwcWalletsLoaded.await().second),
|
defaultNwcWalletId = MutableStateFlow(nwcWalletsLoaded.await().second),
|
||||||
hideDeleteRequestDialog = hideDeleteRequestDialog,
|
hideDeleteRequestDialog = hideDeleteRequestDialog,
|
||||||
@@ -686,6 +670,37 @@ object LocalPreferences {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class FollowListPrefs(
|
||||||
|
val home: TopFilter,
|
||||||
|
val stories: TopFilter,
|
||||||
|
val notification: TopFilter,
|
||||||
|
val discovery: TopFilter,
|
||||||
|
val polls: TopFilter,
|
||||||
|
val pictures: TopFilter,
|
||||||
|
val products: TopFilter,
|
||||||
|
val shorts: TopFilter,
|
||||||
|
val publicChats: TopFilter,
|
||||||
|
val liveStreams: TopFilter,
|
||||||
|
val longs: TopFilter,
|
||||||
|
val articles: TopFilter,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun SharedPreferences.loadFollowListPrefs(): FollowListPrefs =
|
||||||
|
FollowListPrefs(
|
||||||
|
home = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null), TopFilter.AllFollows),
|
||||||
|
stories = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null), TopFilter.Global),
|
||||||
|
notification = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, null), TopFilter.Global),
|
||||||
|
discovery = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null), TopFilter.Global),
|
||||||
|
polls = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_POLLS_FOLLOW_LIST, null), TopFilter.Global),
|
||||||
|
pictures = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_PICTURES_FOLLOW_LIST, null), TopFilter.Global),
|
||||||
|
products = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_PRODUCTS_FOLLOW_LIST, null), TopFilter.AroundMe),
|
||||||
|
shorts = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_SHORTS_FOLLOW_LIST, null), TopFilter.Global),
|
||||||
|
publicChats = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_PUBLIC_CHATS_FOLLOW_LIST, null), TopFilter.Global),
|
||||||
|
liveStreams = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_LIVE_STREAMS_FOLLOW_LIST, null), TopFilter.Global),
|
||||||
|
longs = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_LONGS_FOLLOW_LIST, null), TopFilter.Global),
|
||||||
|
articles = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_ARTICLES_FOLLOW_LIST, null), TopFilter.AllFollows),
|
||||||
|
)
|
||||||
|
|
||||||
private inline fun <reified T : Any> parseOrNull(value: String?): T? {
|
private inline fun <reified T : Any> parseOrNull(value: String?): T? {
|
||||||
if (value.isNullOrEmpty() || value == "null") {
|
if (value.isNullOrEmpty() || value == "null") {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -476,6 +476,12 @@ class Account(
|
|||||||
val liveShortsFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultShortsFollowList)
|
val liveShortsFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultShortsFollowList)
|
||||||
val liveShortsFollowListsPerRelay = OutboxLoaderState(liveShortsFollowLists, cache, scope).flow
|
val liveShortsFollowListsPerRelay = OutboxLoaderState(liveShortsFollowLists, cache, scope).flow
|
||||||
|
|
||||||
|
val livePublicChatsFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultPublicChatsFollowList)
|
||||||
|
val livePublicChatsFollowListsPerRelay = OutboxLoaderState(livePublicChatsFollowLists, cache, scope).flow
|
||||||
|
|
||||||
|
val liveLiveStreamsFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultLiveStreamsFollowList)
|
||||||
|
val liveLiveStreamsFollowListsPerRelay = OutboxLoaderState(liveLiveStreamsFollowLists, cache, scope).flow
|
||||||
|
|
||||||
val liveLongsFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultLongsFollowList)
|
val liveLongsFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultLongsFollowList)
|
||||||
val liveLongsFollowListsPerRelay = OutboxLoaderState(liveLongsFollowLists, cache, scope).flow
|
val liveLongsFollowListsPerRelay = OutboxLoaderState(liveLongsFollowLists, cache, scope).flow
|
||||||
|
|
||||||
|
|||||||
@@ -185,6 +185,8 @@ class AccountSettings(
|
|||||||
val defaultPicturesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
val defaultPicturesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||||
val defaultProductsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.AroundMe),
|
val defaultProductsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.AroundMe),
|
||||||
val defaultShortsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
val defaultShortsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||||
|
val defaultPublicChatsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||||
|
val defaultLiveStreamsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||||
val defaultLongsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
val defaultLongsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||||
val defaultArticlesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.AllFollows),
|
val defaultArticlesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.AllFollows),
|
||||||
val defaultBadgesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Mine),
|
val defaultBadgesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Mine),
|
||||||
@@ -509,6 +511,28 @@ class AccountSettings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun changeDefaultPublicChatsFollowList(name: FeedDefinition) {
|
||||||
|
changeDefaultPublicChatsFollowList(name.code)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeDefaultPublicChatsFollowList(name: TopFilter) {
|
||||||
|
if (defaultPublicChatsFollowList.value != name) {
|
||||||
|
defaultPublicChatsFollowList.tryEmit(name)
|
||||||
|
saveAccountSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeDefaultLiveStreamsFollowList(name: FeedDefinition) {
|
||||||
|
changeDefaultLiveStreamsFollowList(name.code)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeDefaultLiveStreamsFollowList(name: TopFilter) {
|
||||||
|
if (defaultLiveStreamsFollowList.value != name) {
|
||||||
|
defaultLiveStreamsFollowList.tryEmit(name)
|
||||||
|
saveAccountSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun changeDefaultLongsFollowList(name: FeedDefinition) {
|
fun changeDefaultLongsFollowList(name: FeedDefinition) {
|
||||||
changeDefaultLongsFollowList(name.code)
|
changeDefaultLongsFollowList(name.code)
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -39,14 +39,17 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.list.datasource
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.browse.datasource.BrowseEmojiSetsFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.browse.datasource.BrowseEmojiSetsFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource.FollowPackFeedFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource.FollowPackFeedFilterAssembler
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.datasource.FollowPacksFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource.GeoHashFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource.GeoHashFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource.HashtagFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource.HashtagFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssembler
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.datasource.LiveStreamsFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource.LongsFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource.LongsFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource.PicturesFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource.PicturesFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.PollsFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.PollsFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.products.datasource.ProductsFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.products.datasource.ProductsFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource.UserProfileFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource.UserProfileFilterAssembler
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.publicChats.datasource.PublicChatsFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relay.datasource.RelayFeedFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relay.datasource.RelayFeedFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.datasource.RelayInfoNip66FilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.datasource.RelayInfoNip66FilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.datasource.ShortsFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.datasource.ShortsFilterAssembler
|
||||||
@@ -93,12 +96,15 @@ class RelaySubscriptionsCoordinator(
|
|||||||
val relayFeed = RelayFeedFilterAssembler(client)
|
val relayFeed = RelayFeedFilterAssembler(client)
|
||||||
val relayInfoNip66 = RelayInfoNip66FilterAssembler(client)
|
val relayInfoNip66 = RelayInfoNip66FilterAssembler(client)
|
||||||
val followPacks = FollowPackFeedFilterAssembler(client)
|
val followPacks = FollowPackFeedFilterAssembler(client)
|
||||||
|
val followPacksList = FollowPacksFilterAssembler(client)
|
||||||
val chess = ChessFilterAssembler(client)
|
val chess = ChessFilterAssembler(client)
|
||||||
|
|
||||||
val polls = PollsFilterAssembler(client)
|
val polls = PollsFilterAssembler(client)
|
||||||
val pictures = PicturesFilterAssembler(client)
|
val pictures = PicturesFilterAssembler(client)
|
||||||
val products = ProductsFilterAssembler(client)
|
val products = ProductsFilterAssembler(client)
|
||||||
val shorts = ShortsFilterAssembler(client)
|
val shorts = ShortsFilterAssembler(client)
|
||||||
|
val publicChats = PublicChatsFilterAssembler(client)
|
||||||
|
val liveStreams = LiveStreamsFilterAssembler(client)
|
||||||
val longs = LongsFilterAssembler(client)
|
val longs = LongsFilterAssembler(client)
|
||||||
val articles = ArticlesFilterAssembler(client)
|
val articles = ArticlesFilterAssembler(client)
|
||||||
val badges = BadgesFilterAssembler(client)
|
val badges = BadgesFilterAssembler(client)
|
||||||
@@ -120,6 +126,9 @@ class RelaySubscriptionsCoordinator(
|
|||||||
pictures,
|
pictures,
|
||||||
products,
|
products,
|
||||||
shorts,
|
shorts,
|
||||||
|
publicChats,
|
||||||
|
followPacksList,
|
||||||
|
liveStreams,
|
||||||
longs,
|
longs,
|
||||||
articles,
|
articles,
|
||||||
badges,
|
badges,
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ object ScrollStateKeys {
|
|||||||
const val PICTURES_SCREEN = "PicturesFeed"
|
const val PICTURES_SCREEN = "PicturesFeed"
|
||||||
const val PRODUCTS_SCREEN = "ProductsFeed"
|
const val PRODUCTS_SCREEN = "ProductsFeed"
|
||||||
const val SHORTS_SCREEN = "ShortsFeed"
|
const val SHORTS_SCREEN = "ShortsFeed"
|
||||||
|
const val PUBLIC_CHATS_SCREEN = "PublicChatsFeed"
|
||||||
|
const val FOLLOW_PACKS_SCREEN = "FollowPacksFeed"
|
||||||
|
const val LIVE_STREAMS_SCREEN = "LiveStreamsFeed"
|
||||||
const val LONGS_SCREEN = "LongsFeed"
|
const val LONGS_SCREEN = "LongsFeed"
|
||||||
const val ARTICLES_SCREEN = "ArticlesFeed"
|
const val ARTICLES_SCREEN = "ArticlesFeed"
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.list.metadata.Em
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.membershipManagement.EmojiPackSelectionScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.membershipManagement.EmojiPackSelectionScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.membershipManagement.MyEmojiListScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.membershipManagement.MyEmojiListScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.FollowPackFeedScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.FollowPackFeedScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.FollowPacksScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashPostScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashPostScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagPostScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagPostScreen
|
||||||
@@ -127,6 +128,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.ListOfPeopleList
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.FollowPackMetadataScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.FollowPackMetadataScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.PeopleListMetadataScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.PeopleListMetadataScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.memberEdit.FollowListAndPackAndUserScreen
|
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.longs.LongsScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser.ImportFollowListPickFollowsScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser.ImportFollowListPickFollowsScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser.ImportFollowListSelectUserScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser.ImportFollowListSelectUserScreen
|
||||||
@@ -139,6 +141,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.PollsScreen
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy.PrivacyOptionsScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy.PrivacyOptionsScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.products.ProductsScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.products.ProductsScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.publicChats.PublicChatsScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ShowQRScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ShowQRScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.redirect.LoadRedirectScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.redirect.LoadRedirectScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relay.RelayFeedScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relay.RelayFeedScreen
|
||||||
@@ -240,6 +243,9 @@ fun BuildNavigation(
|
|||||||
composableFromEnd<Route.Pictures> { PicturesScreen(accountViewModel, nav) }
|
composableFromEnd<Route.Pictures> { PicturesScreen(accountViewModel, nav) }
|
||||||
composableFromEnd<Route.Products> { ProductsScreen(accountViewModel, nav) }
|
composableFromEnd<Route.Products> { ProductsScreen(accountViewModel, nav) }
|
||||||
composableFromEnd<Route.Shorts> { ShortsScreen(accountViewModel, nav) }
|
composableFromEnd<Route.Shorts> { ShortsScreen(accountViewModel, nav) }
|
||||||
|
composableFromEnd<Route.PublicChats> { PublicChatsScreen(accountViewModel, nav) }
|
||||||
|
composableFromEnd<Route.FollowPacks> { FollowPacksScreen(accountViewModel, nav) }
|
||||||
|
composableFromEnd<Route.LiveStreams> { LiveStreamsScreen(accountViewModel, nav) }
|
||||||
composableFromEnd<Route.Longs> { LongsScreen(accountViewModel, nav) }
|
composableFromEnd<Route.Longs> { LongsScreen(accountViewModel, nav) }
|
||||||
composableFromEnd<Route.Articles> { ArticlesScreen(accountViewModel, nav) }
|
composableFromEnd<Route.Articles> { ArticlesScreen(accountViewModel, nav) }
|
||||||
composableFromEnd<Route.NewHlsVideo> { NewHlsVideoScreen(accountViewModel, nav) }
|
composableFromEnd<Route.NewHlsVideo> { NewHlsVideoScreen(accountViewModel, nav) }
|
||||||
|
|||||||
+26
@@ -53,6 +53,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
|
import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
|
||||||
import androidx.compose.material.icons.automirrored.filled.Send
|
import androidx.compose.material.icons.automirrored.filled.Send
|
||||||
import androidx.compose.material.icons.automirrored.outlined.Article
|
import androidx.compose.material.icons.automirrored.outlined.Article
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.Chat
|
||||||
import androidx.compose.material.icons.filled.AccountCircle
|
import androidx.compose.material.icons.filled.AccountCircle
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.outlined.AccountBalanceWallet
|
import androidx.compose.material.icons.outlined.AccountBalanceWallet
|
||||||
@@ -67,6 +68,7 @@ import androidx.compose.material.icons.outlined.Language
|
|||||||
import androidx.compose.material.icons.outlined.MilitaryTech
|
import androidx.compose.material.icons.outlined.MilitaryTech
|
||||||
import androidx.compose.material.icons.outlined.Photo
|
import androidx.compose.material.icons.outlined.Photo
|
||||||
import androidx.compose.material.icons.outlined.PlayCircle
|
import androidx.compose.material.icons.outlined.PlayCircle
|
||||||
|
import androidx.compose.material.icons.outlined.Sensors
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
import androidx.compose.material.icons.outlined.SettingsInputAntenna
|
import androidx.compose.material.icons.outlined.SettingsInputAntenna
|
||||||
import androidx.compose.material.icons.outlined.SmartDisplay
|
import androidx.compose.material.icons.outlined.SmartDisplay
|
||||||
@@ -695,6 +697,30 @@ fun ListContent(
|
|||||||
route = Route.Shorts,
|
route = Route.Shorts,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
NavigationRow(
|
||||||
|
title = R.string.public_chats,
|
||||||
|
icon = Icons.AutoMirrored.Outlined.Chat,
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
|
nav = nav,
|
||||||
|
route = Route.PublicChats,
|
||||||
|
)
|
||||||
|
|
||||||
|
NavigationRow(
|
||||||
|
title = R.string.follow_packs,
|
||||||
|
icon = Icons.Outlined.CollectionsBookmark,
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
|
nav = nav,
|
||||||
|
route = Route.FollowPacks,
|
||||||
|
)
|
||||||
|
|
||||||
|
NavigationRow(
|
||||||
|
title = R.string.live_streams,
|
||||||
|
icon = Icons.Outlined.Sensors,
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
|
nav = nav,
|
||||||
|
route = Route.LiveStreams,
|
||||||
|
)
|
||||||
|
|
||||||
NavigationRow(
|
NavigationRow(
|
||||||
title = R.string.longs,
|
title = R.string.longs,
|
||||||
icon = Icons.Outlined.SmartDisplay,
|
icon = Icons.Outlined.SmartDisplay,
|
||||||
|
|||||||
@@ -83,6 +83,12 @@ sealed class Route {
|
|||||||
|
|
||||||
@Serializable object Shorts : Route()
|
@Serializable object Shorts : Route()
|
||||||
|
|
||||||
|
@Serializable object PublicChats : Route()
|
||||||
|
|
||||||
|
@Serializable object FollowPacks : Route()
|
||||||
|
|
||||||
|
@Serializable object LiveStreams : Route()
|
||||||
|
|
||||||
@Serializable object Longs : Route()
|
@Serializable object Longs : Route()
|
||||||
|
|
||||||
@Serializable object Articles : Route()
|
@Serializable object Articles : Route()
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.note.types
|
package com.vitorpamplona.amethyst.ui.note.types
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -61,11 +60,10 @@ import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground
|
|||||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
|
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
import com.vitorpamplona.amethyst.ui.theme.Font10SP
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
|
||||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||||
|
|
||||||
private const val WORDS_PER_MINUTE = 225
|
private const val WORDS_PER_MINUTE = 225
|
||||||
@@ -107,16 +105,6 @@ fun LongFormHeader(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||||
) {
|
) {
|
||||||
if (topics.isNotEmpty()) {
|
|
||||||
FlowRow(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(Size5dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(Size5dp),
|
|
||||||
modifier = Modifier.padding(bottom = 8.dp),
|
|
||||||
) {
|
|
||||||
topics.forEach { TopicChip(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
title?.let {
|
title?.let {
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
@@ -142,6 +130,16 @@ fun LongFormHeader(
|
|||||||
|
|
||||||
Spacer(Modifier.padding(top = 12.dp))
|
Spacer(Modifier.padding(top = 12.dp))
|
||||||
AuthorMetaRow(note, readingMinutes, accountViewModel)
|
AuthorMetaRow(note, readingMinutes, accountViewModel)
|
||||||
|
|
||||||
|
if (topics.isNotEmpty()) {
|
||||||
|
Spacer(Modifier.padding(top = 8.dp))
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Size5dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Size5dp),
|
||||||
|
) {
|
||||||
|
topics.forEach { TopicChip(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,17 +180,16 @@ private fun TopicChip(topic: String) {
|
|||||||
Text(
|
Text(
|
||||||
text = "#$topic",
|
text = "#$topic",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
fontSize = Font12SP,
|
fontSize = Font10SP,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Normal,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.grayText,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.border(1.dp, MaterialTheme.colorScheme.subtleBorder, CircleShape)
|
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f))
|
||||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.08f))
|
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||||
.padding(horizontal = 10.dp, vertical = 4.dp),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
@@ -41,9 +41,11 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.Discover
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.DiscoverMarketplaceFeedFilter
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.DiscoverMarketplaceFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.dal.DraftEventsFeedFilter
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.dal.DraftEventsFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.browse.dal.BrowseEmojiSetsFeedFilter
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.browse.dal.BrowseEmojiSetsFeedFilter
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.dal.FollowPacksFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeConversationsFeedFilter
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeConversationsFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeLiveFilter
|
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.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.longs.dal.LongsFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedContentState
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedContentState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationSummaryState
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationSummaryState
|
||||||
@@ -54,6 +56,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.dal.ClosedPollsFeedFi
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.dal.OpenPollsFeedFilter
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.dal.OpenPollsFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.dal.PollsFeedFilter
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.dal.PollsFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.products.dal.ProductsFeedFilter
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.products.dal.ProductsFeedFilter
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.publicChats.dal.PublicChatsFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.dal.ShortsFeedFilter
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.dal.ShortsFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal.VideoFeedFilter
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal.VideoFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.webBookmarks.dal.WebBookmarkFeedFilter
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.webBookmarks.dal.WebBookmarkFeedFilter
|
||||||
@@ -94,6 +97,9 @@ class AccountFeedContentStates(
|
|||||||
val picturesFeed = FeedContentState(PictureFeedFilter(account), scope, LocalCache)
|
val picturesFeed = FeedContentState(PictureFeedFilter(account), scope, LocalCache)
|
||||||
val productsFeed = FeedContentState(ProductsFeedFilter(account), scope, LocalCache)
|
val productsFeed = FeedContentState(ProductsFeedFilter(account), scope, LocalCache)
|
||||||
val shortsFeed = FeedContentState(ShortsFeedFilter(account), scope, LocalCache)
|
val shortsFeed = FeedContentState(ShortsFeedFilter(account), scope, LocalCache)
|
||||||
|
val publicChatsFeed = FeedContentState(PublicChatsFeedFilter(account), scope, LocalCache)
|
||||||
|
val followPacksFeed = FeedContentState(FollowPacksFeedFilter(account), scope, LocalCache)
|
||||||
|
val liveStreamsFeed = FeedContentState(LiveStreamsFeedFilter(account), scope, LocalCache)
|
||||||
val longsFeed = FeedContentState(LongsFeedFilter(account), scope, LocalCache)
|
val longsFeed = FeedContentState(LongsFeedFilter(account), scope, LocalCache)
|
||||||
val articlesFeed = FeedContentState(ArticlesFeedFilter(account), scope, LocalCache)
|
val articlesFeed = FeedContentState(ArticlesFeedFilter(account), scope, LocalCache)
|
||||||
|
|
||||||
@@ -156,6 +162,9 @@ class AccountFeedContentStates(
|
|||||||
picturesFeed.updateFeedWith(newNotes)
|
picturesFeed.updateFeedWith(newNotes)
|
||||||
productsFeed.updateFeedWith(newNotes)
|
productsFeed.updateFeedWith(newNotes)
|
||||||
shortsFeed.updateFeedWith(newNotes)
|
shortsFeed.updateFeedWith(newNotes)
|
||||||
|
publicChatsFeed.updateFeedWith(newNotes)
|
||||||
|
followPacksFeed.updateFeedWith(newNotes)
|
||||||
|
liveStreamsFeed.updateFeedWith(newNotes)
|
||||||
longsFeed.updateFeedWith(newNotes)
|
longsFeed.updateFeedWith(newNotes)
|
||||||
articlesFeed.updateFeedWith(newNotes)
|
articlesFeed.updateFeedWith(newNotes)
|
||||||
|
|
||||||
@@ -199,6 +208,9 @@ class AccountFeedContentStates(
|
|||||||
picturesFeed.deleteFromFeed(newNotes)
|
picturesFeed.deleteFromFeed(newNotes)
|
||||||
productsFeed.deleteFromFeed(newNotes)
|
productsFeed.deleteFromFeed(newNotes)
|
||||||
shortsFeed.deleteFromFeed(newNotes)
|
shortsFeed.deleteFromFeed(newNotes)
|
||||||
|
publicChatsFeed.deleteFromFeed(newNotes)
|
||||||
|
followPacksFeed.deleteFromFeed(newNotes)
|
||||||
|
liveStreamsFeed.deleteFromFeed(newNotes)
|
||||||
longsFeed.deleteFromFeed(newNotes)
|
longsFeed.deleteFromFeed(newNotes)
|
||||||
articlesFeed.deleteFromFeed(newNotes)
|
articlesFeed.deleteFromFeed(newNotes)
|
||||||
|
|
||||||
|
|||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* 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.followPacks.list
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
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.ui.layouts.rememberFeedContentPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun FollowPacksFeedLoaded(
|
||||||
|
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 },
|
||||||
|
contentType = { _, item -> item.event?.kind ?: -1 },
|
||||||
|
) { _, item ->
|
||||||
|
Row(Modifier.fillMaxWidth().animateItem()) {
|
||||||
|
ChannelCardCompose(
|
||||||
|
baseNote = item,
|
||||||
|
routeForLastRead = "FollowPacksFeed",
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
forceEventKind = FollowListEvent.KIND,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = DividerThickness,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* 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.followPacks.list
|
||||||
|
|
||||||
|
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.followPacks.list.datasource.FollowPacksFilterAssemblerSubscription
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FollowPacksScreen(
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
FollowPacksScreen(
|
||||||
|
followPacksFeedContentState = accountViewModel.feedStates.followPacksFeed,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FollowPacksScreen(
|
||||||
|
followPacksFeedContentState: FeedContentState,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
WatchLifecycleAndUpdateModel(followPacksFeedContentState)
|
||||||
|
WatchAccountForFollowPacksScreen(feedState = followPacksFeedContentState, accountViewModel = accountViewModel)
|
||||||
|
FollowPacksFilterAssemblerSubscription(accountViewModel)
|
||||||
|
|
||||||
|
DisappearingScaffold(
|
||||||
|
isInvertedLayout = false,
|
||||||
|
topBar = {
|
||||||
|
FollowPacksTopBar(accountViewModel, nav)
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
AppBottomBar(Route.FollowPacks, accountViewModel) { route ->
|
||||||
|
if (route == Route.FollowPacks) {
|
||||||
|
followPacksFeedContentState.sendToTop()
|
||||||
|
} else {
|
||||||
|
nav.newStack(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
) {
|
||||||
|
RefresheableBox(followPacksFeedContentState, true) {
|
||||||
|
SaveableFeedContentState(followPacksFeedContentState, scrollStateKey = ScrollStateKeys.FOLLOW_PACKS_SCREEN) { listState ->
|
||||||
|
RenderFeedContentState(
|
||||||
|
feedContentState = followPacksFeedContentState,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
listState = listState,
|
||||||
|
nav = nav,
|
||||||
|
routeForLastRead = "FollowPacksFeed",
|
||||||
|
onLoaded = { loaded ->
|
||||||
|
FollowPacksFeedLoaded(
|
||||||
|
loaded = loaded,
|
||||||
|
listState = listState,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WatchAccountForFollowPacksScreen(
|
||||||
|
feedState: FeedContentState,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
val listState by accountViewModel.account.liveDiscoveryFollowLists.collectAsStateWithLifecycle()
|
||||||
|
val hiddenUsers =
|
||||||
|
accountViewModel.account.hiddenUsers.flow
|
||||||
|
.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
LaunchedEffect(accountViewModel, listState, hiddenUsers) {
|
||||||
|
feedState.checkKeysInvalidateDataAndSendToTop()
|
||||||
|
}
|
||||||
|
}
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* 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.followPacks.list
|
||||||
|
|
||||||
|
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 FollowPacksTopBar(
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
UserDrawerSearchTopBar(accountViewModel, nav) {
|
||||||
|
val list by accountViewModel.account.settings.defaultDiscoveryFollowList
|
||||||
|
.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
FollowPacksTopNavFilterBar(
|
||||||
|
followListsModel = accountViewModel.feedStates.feedListOptions,
|
||||||
|
listName = list,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
onChange = accountViewModel.account.settings::changeDefaultDiscoveryFollowList,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FollowPacksTopNavFilterBar(
|
||||||
|
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(allLists.getOrNull(it) ?: followListsModel.allFollows) },
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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.followPacks.list.dal
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.DiscoverFollowSetsFeedFilter
|
||||||
|
|
||||||
|
class FollowPacksFeedFilter(
|
||||||
|
account: Account,
|
||||||
|
) : DiscoverFollowSetsFeedFilter(account) {
|
||||||
|
override fun feedKey(): String = "follow-packs-" + account.userProfile().pubkeyHex + "-" + followList().code
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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.followPacks.list.datasource
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
|
class FollowPacksQueryState(
|
||||||
|
val account: Account,
|
||||||
|
val feedStates: AccountFeedContentStates,
|
||||||
|
val scope: CoroutineScope,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class FollowPacksFilterAssembler(
|
||||||
|
client: INostrClient,
|
||||||
|
) : ComposeSubscriptionManager<FollowPacksQueryState>() {
|
||||||
|
val group =
|
||||||
|
listOf(
|
||||||
|
FollowPacksSubAssembler(client, ::allKeys),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun invalidateKeys() = invalidateFilters()
|
||||||
|
|
||||||
|
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
|
||||||
|
|
||||||
|
override fun destroy() = group.forEach { it.destroy() }
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* 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.followPacks.list.datasource
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FollowPacksFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
|
||||||
|
FollowPacksFilterAssemblerSubscription(
|
||||||
|
accountViewModel.dataSources().followPacksList,
|
||||||
|
accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FollowPacksFilterAssemblerSubscription(
|
||||||
|
dataSource: FollowPacksFilterAssembler,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
val state =
|
||||||
|
remember(accountViewModel.account) {
|
||||||
|
FollowPacksQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyDataSourceSubscription(state, dataSource)
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* 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.followPacks.list.datasource
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.TopFilter
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.makeFollowSetsFilter
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.sample
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class FollowPacksSubAssembler(
|
||||||
|
client: INostrClient,
|
||||||
|
allKeys: () -> Set<FollowPacksQueryState>,
|
||||||
|
) : PerUserAndFollowListEoseManager<FollowPacksQueryState, TopFilter>(client, allKeys) {
|
||||||
|
override fun updateFilter(
|
||||||
|
key: FollowPacksQueryState,
|
||||||
|
since: SincePerRelayMap?,
|
||||||
|
): List<RelayBasedFilter> {
|
||||||
|
val feedSettings = key.followsPerRelay()
|
||||||
|
|
||||||
|
return makeFollowSetsFilter(feedSettings, since, key.feedStates.followPacksFeed.lastNoteCreatedAtIfFilled())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun user(key: FollowPacksQueryState) = key.account.userProfile()
|
||||||
|
|
||||||
|
override fun list(key: FollowPacksQueryState) = key.listName()
|
||||||
|
|
||||||
|
fun FollowPacksQueryState.listNameFlow() = account.settings.defaultDiscoveryFollowList
|
||||||
|
|
||||||
|
fun FollowPacksQueryState.listName() = listNameFlow().value
|
||||||
|
|
||||||
|
fun FollowPacksQueryState.followsPerRelayFlow() = account.liveDiscoveryFollowListsPerRelay
|
||||||
|
|
||||||
|
fun FollowPacksQueryState.followsPerRelay() = followsPerRelayFlow().value
|
||||||
|
|
||||||
|
val userJobMap = mutableMapOf<User, List<Job>>()
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
override fun newSub(key: FollowPacksQueryState): Subscription {
|
||||||
|
val user = user(key)
|
||||||
|
userJobMap[user]?.forEach { it.cancel() }
|
||||||
|
userJobMap[user] =
|
||||||
|
listOf(
|
||||||
|
key.scope.launch(Dispatchers.IO) {
|
||||||
|
key.listNameFlow().collectLatest {
|
||||||
|
invalidateFilters()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
key.scope.launch(Dispatchers.IO) {
|
||||||
|
key.followsPerRelayFlow().sample(500).collectLatest {
|
||||||
|
invalidateFilters()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
key.account.scope.launch(Dispatchers.IO) {
|
||||||
|
key.feedStates.followPacksFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
|
||||||
|
invalidateFilters()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return super.newSub(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun endSub(
|
||||||
|
key: User,
|
||||||
|
subId: String,
|
||||||
|
) {
|
||||||
|
super.endSub(key, subId)
|
||||||
|
userJobMap[key]?.forEach { it.cancel() }
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* 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.livestreams
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
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.ui.layouts.rememberFeedContentPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||||
|
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun LiveStreamsFeedLoaded(
|
||||||
|
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()) {
|
||||||
|
ChannelCardCompose(
|
||||||
|
baseNote = item,
|
||||||
|
routeForLastRead = "LiveStreamsFeed",
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
forceEventKind = LiveActivitiesEvent.KIND,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = DividerThickness,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* 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.livestreams
|
||||||
|
|
||||||
|
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.livestreams.datasource.LiveStreamsFilterAssemblerSubscription
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LiveStreamsScreen(
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
LiveStreamsScreen(
|
||||||
|
liveStreamsFeedContentState = accountViewModel.feedStates.liveStreamsFeed,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LiveStreamsScreen(
|
||||||
|
liveStreamsFeedContentState: FeedContentState,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
WatchLifecycleAndUpdateModel(liveStreamsFeedContentState)
|
||||||
|
WatchAccountForLiveStreamsScreen(liveStreamsFeedState = liveStreamsFeedContentState, accountViewModel = accountViewModel)
|
||||||
|
LiveStreamsFilterAssemblerSubscription(accountViewModel)
|
||||||
|
|
||||||
|
DisappearingScaffold(
|
||||||
|
isInvertedLayout = false,
|
||||||
|
topBar = {
|
||||||
|
LiveStreamsTopBar(accountViewModel, nav)
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
AppBottomBar(Route.LiveStreams, accountViewModel) { route ->
|
||||||
|
if (route == Route.LiveStreams) {
|
||||||
|
liveStreamsFeedContentState.sendToTop()
|
||||||
|
} else {
|
||||||
|
nav.newStack(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
) {
|
||||||
|
RefresheableBox(liveStreamsFeedContentState, true) {
|
||||||
|
SaveableFeedContentState(liveStreamsFeedContentState, scrollStateKey = ScrollStateKeys.LIVE_STREAMS_SCREEN) { listState ->
|
||||||
|
RenderFeedContentState(
|
||||||
|
feedContentState = liveStreamsFeedContentState,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
listState = listState,
|
||||||
|
nav = nav,
|
||||||
|
routeForLastRead = "LiveStreamsFeed",
|
||||||
|
onLoaded = { loaded ->
|
||||||
|
LiveStreamsFeedLoaded(
|
||||||
|
loaded = loaded,
|
||||||
|
listState = listState,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WatchAccountForLiveStreamsScreen(
|
||||||
|
liveStreamsFeedState: FeedContentState,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
val listState by accountViewModel.account.liveLiveStreamsFollowLists.collectAsStateWithLifecycle()
|
||||||
|
val hiddenUsers =
|
||||||
|
accountViewModel.account.hiddenUsers.flow
|
||||||
|
.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
LaunchedEffect(accountViewModel, listState, hiddenUsers) {
|
||||||
|
liveStreamsFeedState.checkKeysInvalidateDataAndSendToTop()
|
||||||
|
}
|
||||||
|
}
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* 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.livestreams
|
||||||
|
|
||||||
|
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 LiveStreamsTopBar(
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
UserDrawerSearchTopBar(accountViewModel, nav) {
|
||||||
|
val list by accountViewModel.account.settings.defaultLiveStreamsFollowList
|
||||||
|
.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
LiveStreamsTopNavFilterBar(
|
||||||
|
followListsModel = accountViewModel.feedStates.feedListOptions,
|
||||||
|
listName = list,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
onChange = accountViewModel.account.settings::changeDefaultLiveStreamsFollowList,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LiveStreamsTopNavFilterBar(
|
||||||
|
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(allLists.getOrNull(it) ?: followListsModel.allFollows) },
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
+174
@@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
* 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.livestreams.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.service.OnlineChecker
|
||||||
|
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.meetingSpaces.MeetingSpaceEvent
|
||||||
|
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||||
|
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag
|
||||||
|
|
||||||
|
class LiveStreamsFeedFilter(
|
||||||
|
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
|
||||||
|
|
||||||
|
fun TopFilter.isMuteList() = this is TopFilter.MuteList
|
||||||
|
|
||||||
|
fun TopFilter.isBlockList() = this is TopFilter.PeopleList && this.address == account.blockPeopleList.getBlockListAddress()
|
||||||
|
|
||||||
|
fun TopFilter.wantsToSeeNegativeStuff() = isMuteList() || isBlockList()
|
||||||
|
|
||||||
|
override fun showHiddenKey(): Boolean = followList().wantsToSeeNegativeStuff()
|
||||||
|
|
||||||
|
override fun feed(): List<Note> {
|
||||||
|
val allChannelNotes = LocalCache.liveChatChannels.mapNotNull { _, channel -> LocalCache.getAddressableNoteIfExists(channel.address) }
|
||||||
|
val allMessageNotes = LocalCache.liveChatChannels.map { _, channel -> channel.notes.filter { key, it -> it.event is LiveActivitiesEvent } }.flatten()
|
||||||
|
|
||||||
|
val notes = innerApplyFilter(allChannelNotes + allMessageNotes)
|
||||||
|
|
||||||
|
return sort(notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
(noteEvent is LiveActivitiesEvent || noteEvent is MeetingSpaceEvent || noteEvent is MeetingRoomEvent) &&
|
||||||
|
filterParams.match(noteEvent, it.relays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sort(items: Set<Note>): List<Note> {
|
||||||
|
val topFilter = account.liveLiveStreamsFollowLists.value
|
||||||
|
val liveStreamsTopFilterAuthors =
|
||||||
|
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 =
|
||||||
|
liveStreamsTopFilterAuthors ?: 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) },
|
||||||
|
{ participantCounts[it] },
|
||||||
|
{ allParticipants[it] },
|
||||||
|
{
|
||||||
|
when (val e = it.event) {
|
||||||
|
is LiveActivitiesEvent -> e.starts() ?: it.createdAt()
|
||||||
|
is MeetingRoomEvent -> e.starts() ?: it.createdAt()
|
||||||
|
else -> it.createdAt()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ it.idHex },
|
||||||
|
),
|
||||||
|
).reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertStatusToOrder(event: com.vitorpamplona.quartz.nip01Core.core.Event?): Int {
|
||||||
|
if (event == null) return 0
|
||||||
|
return when (event) {
|
||||||
|
is LiveActivitiesEvent -> {
|
||||||
|
val url = event.streaming() ?: return 0
|
||||||
|
when (event.status()) {
|
||||||
|
StatusTag.STATUS.LIVE -> {
|
||||||
|
if (OnlineChecker.isCachedAndOffline(url)) 0 else 2
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusTag.STATUS.PLANNED -> {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusTag.STATUS.ENDED -> {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is MeetingRoomEvent -> {
|
||||||
|
when (event.status()) {
|
||||||
|
StatusTag.STATUS.LIVE -> 2
|
||||||
|
StatusTag.STATUS.PLANNED -> 1
|
||||||
|
StatusTag.STATUS.ENDED -> 0
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is MeetingSpaceEvent -> {
|
||||||
|
when (event.status()) {
|
||||||
|
com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag.STATUS.OPEN -> 2
|
||||||
|
com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag.STATUS.PRIVATE -> 1
|
||||||
|
com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag.STATUS.CLOSED -> 0
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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.livestreams.datasource
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
|
class LiveStreamsQueryState(
|
||||||
|
val account: Account,
|
||||||
|
val feedStates: AccountFeedContentStates,
|
||||||
|
val scope: CoroutineScope,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class LiveStreamsFilterAssembler(
|
||||||
|
client: INostrClient,
|
||||||
|
) : ComposeSubscriptionManager<LiveStreamsQueryState>() {
|
||||||
|
val group =
|
||||||
|
listOf(
|
||||||
|
LiveStreamsSubAssembler(client, ::allKeys),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun invalidateKeys() = invalidateFilters()
|
||||||
|
|
||||||
|
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
|
||||||
|
|
||||||
|
override fun destroy() = group.forEach { it.destroy() }
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* 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.livestreams.datasource
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LiveStreamsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
|
||||||
|
LiveStreamsFilterAssemblerSubscription(
|
||||||
|
accountViewModel.dataSources().liveStreams,
|
||||||
|
accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LiveStreamsFilterAssemblerSubscription(
|
||||||
|
dataSource: LiveStreamsFilterAssembler,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
val state =
|
||||||
|
remember(accountViewModel.account) {
|
||||||
|
LiveStreamsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyDataSourceSubscription(state, dataSource)
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* 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.livestreams.datasource
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.TopFilter
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.makeLiveActivitiesFilter
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.sample
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class LiveStreamsSubAssembler(
|
||||||
|
client: INostrClient,
|
||||||
|
allKeys: () -> Set<LiveStreamsQueryState>,
|
||||||
|
) : PerUserAndFollowListEoseManager<LiveStreamsQueryState, TopFilter>(client, allKeys) {
|
||||||
|
override fun updateFilter(
|
||||||
|
key: LiveStreamsQueryState,
|
||||||
|
since: SincePerRelayMap?,
|
||||||
|
): List<RelayBasedFilter> {
|
||||||
|
val feedSettings = key.followsPerRelay()
|
||||||
|
|
||||||
|
return makeLiveActivitiesFilter(feedSettings, since, key.feedStates.liveStreamsFeed.lastNoteCreatedAtIfFilled())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun user(key: LiveStreamsQueryState) = key.account.userProfile()
|
||||||
|
|
||||||
|
override fun list(key: LiveStreamsQueryState) = key.listName()
|
||||||
|
|
||||||
|
fun LiveStreamsQueryState.listNameFlow() = account.settings.defaultLiveStreamsFollowList
|
||||||
|
|
||||||
|
fun LiveStreamsQueryState.listName() = listNameFlow().value
|
||||||
|
|
||||||
|
fun LiveStreamsQueryState.followsPerRelayFlow() = account.liveLiveStreamsFollowListsPerRelay
|
||||||
|
|
||||||
|
fun LiveStreamsQueryState.followsPerRelay() = followsPerRelayFlow().value
|
||||||
|
|
||||||
|
val userJobMap = mutableMapOf<User, List<Job>>()
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
override fun newSub(key: LiveStreamsQueryState): Subscription {
|
||||||
|
val user = user(key)
|
||||||
|
userJobMap[user]?.forEach { it.cancel() }
|
||||||
|
userJobMap[user] =
|
||||||
|
listOf(
|
||||||
|
key.scope.launch(Dispatchers.IO) {
|
||||||
|
key.listNameFlow().collectLatest {
|
||||||
|
invalidateFilters()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
key.scope.launch(Dispatchers.IO) {
|
||||||
|
key.followsPerRelayFlow().sample(500).collectLatest {
|
||||||
|
invalidateFilters()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
key.account.scope.launch(Dispatchers.IO) {
|
||||||
|
key.feedStates.liveStreamsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
|
||||||
|
invalidateFilters()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return super.newSub(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun endSub(
|
||||||
|
key: User,
|
||||||
|
subId: String,
|
||||||
|
) {
|
||||||
|
super.endSub(key, subId)
|
||||||
|
userJobMap[key]?.forEach { it.cancel() }
|
||||||
|
}
|
||||||
|
}
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* 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.publicChats
|
||||||
|
|
||||||
|
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.ui.layouts.rememberFeedContentPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||||
|
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PublicChatsFeedLoaded(
|
||||||
|
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 },
|
||||||
|
contentType = { _, item -> item.event?.kind ?: -1 },
|
||||||
|
) { _, item ->
|
||||||
|
Row(Modifier.fillMaxWidth().animateItem()) {
|
||||||
|
ChannelCardCompose(
|
||||||
|
baseNote = item,
|
||||||
|
routeForLastRead = "PublicChatsFeed",
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
forceEventKind = ChannelCreateEvent.KIND,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = DividerThickness,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* 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.publicChats
|
||||||
|
|
||||||
|
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.publicChats.datasource.PublicChatsFilterAssemblerSubscription
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PublicChatsScreen(
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
PublicChatsScreen(
|
||||||
|
publicChatsFeedContentState = accountViewModel.feedStates.publicChatsFeed,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PublicChatsScreen(
|
||||||
|
publicChatsFeedContentState: FeedContentState,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
WatchLifecycleAndUpdateModel(publicChatsFeedContentState)
|
||||||
|
WatchAccountForPublicChatsScreen(publicChatsFeedState = publicChatsFeedContentState, accountViewModel = accountViewModel)
|
||||||
|
PublicChatsFilterAssemblerSubscription(accountViewModel)
|
||||||
|
|
||||||
|
DisappearingScaffold(
|
||||||
|
isInvertedLayout = false,
|
||||||
|
topBar = {
|
||||||
|
PublicChatsTopBar(accountViewModel, nav)
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
AppBottomBar(Route.PublicChats, accountViewModel) { route ->
|
||||||
|
if (route == Route.PublicChats) {
|
||||||
|
publicChatsFeedContentState.sendToTop()
|
||||||
|
} else {
|
||||||
|
nav.newStack(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
) {
|
||||||
|
RefresheableBox(publicChatsFeedContentState, true) {
|
||||||
|
SaveableFeedContentState(publicChatsFeedContentState, scrollStateKey = ScrollStateKeys.PUBLIC_CHATS_SCREEN) { listState ->
|
||||||
|
RenderFeedContentState(
|
||||||
|
feedContentState = publicChatsFeedContentState,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
listState = listState,
|
||||||
|
nav = nav,
|
||||||
|
routeForLastRead = "PublicChatsFeed",
|
||||||
|
onLoaded = { loaded ->
|
||||||
|
PublicChatsFeedLoaded(
|
||||||
|
loaded = loaded,
|
||||||
|
listState = listState,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WatchAccountForPublicChatsScreen(
|
||||||
|
publicChatsFeedState: FeedContentState,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
val listState by accountViewModel.account.livePublicChatsFollowLists.collectAsStateWithLifecycle()
|
||||||
|
val hiddenUsers =
|
||||||
|
accountViewModel.account.hiddenUsers.flow
|
||||||
|
.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
LaunchedEffect(accountViewModel, listState, hiddenUsers) {
|
||||||
|
publicChatsFeedState.checkKeysInvalidateDataAndSendToTop()
|
||||||
|
}
|
||||||
|
}
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* 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.publicChats
|
||||||
|
|
||||||
|
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 PublicChatsTopBar(
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
UserDrawerSearchTopBar(accountViewModel, nav) {
|
||||||
|
val list by accountViewModel.account.settings.defaultPublicChatsFollowList
|
||||||
|
.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
PublicChatsTopNavFilterBar(
|
||||||
|
followListsModel = accountViewModel.feedStates.feedListOptions,
|
||||||
|
listName = list,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
onChange = accountViewModel.account.settings::changeDefaultPublicChatsFollowList,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PublicChatsTopNavFilterBar(
|
||||||
|
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(allLists.getOrNull(it) ?: followListsModel.allFollows) },
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
* 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.publicChats.dal
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.TopFilter
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
||||||
|
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||||
|
import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
|
||||||
|
|
||||||
|
class PublicChatsFeedFilter(
|
||||||
|
val account: Account,
|
||||||
|
) : AdditiveFeedFilter<Note>() {
|
||||||
|
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList().code
|
||||||
|
|
||||||
|
override fun limit() = 200
|
||||||
|
|
||||||
|
fun followList(): TopFilter = account.settings.defaultPublicChatsFollowList.value
|
||||||
|
|
||||||
|
fun TopFilter.isMuteList() = this is TopFilter.MuteList
|
||||||
|
|
||||||
|
fun TopFilter.isBlockList() = this is TopFilter.PeopleList && this.address == account.blockPeopleList.getBlockListAddress()
|
||||||
|
|
||||||
|
fun TopFilter.wantsToSeeNegativeStuff() = isMuteList() || isBlockList()
|
||||||
|
|
||||||
|
override fun showHiddenKey(): Boolean = followList().wantsToSeeNegativeStuff()
|
||||||
|
|
||||||
|
override fun feed(): List<Note> {
|
||||||
|
val params = buildFilterParams(account)
|
||||||
|
|
||||||
|
val allChannelNotes =
|
||||||
|
LocalCache.publicChatChannels.mapNotNullIntoSet { _, channel ->
|
||||||
|
val note = LocalCache.getNoteIfExists(channel.idHex)
|
||||||
|
val noteEvent = note?.event
|
||||||
|
|
||||||
|
if (noteEvent == null || params.match(noteEvent, note.relays)) {
|
||||||
|
note
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sort(allChannelNotes)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applyFilter(newItems: Set<Note>): Set<Note> = innerApplyFilter(newItems)
|
||||||
|
|
||||||
|
fun buildFilterParams(account: Account): FilterByListParams =
|
||||||
|
FilterByListParams.create(
|
||||||
|
followLists = account.livePublicChatsFollowLists.value,
|
||||||
|
hiddenUsers = account.hiddenUsers.flow.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
|
val params = buildFilterParams(account)
|
||||||
|
|
||||||
|
return collection.mapNotNullTo(HashSet()) { note ->
|
||||||
|
val noteEvent = note.event
|
||||||
|
if (noteEvent is ChannelCreateEvent && params.match(noteEvent, note.relays)) {
|
||||||
|
if ((LocalCache.getPublicChatChannelIfExists(noteEvent.id)?.notes?.size() ?: 0) > 0) {
|
||||||
|
note
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else if (noteEvent is IsInPublicChatChannel) {
|
||||||
|
val channel = noteEvent.channelId()?.let { LocalCache.checkGetOrCreateNote(it) }
|
||||||
|
val channelEvent = channel?.event
|
||||||
|
|
||||||
|
if (channel != null &&
|
||||||
|
(channelEvent == null || (channelEvent is ChannelCreateEvent && params.match(channelEvent, note.relays)))
|
||||||
|
) {
|
||||||
|
if ((LocalCache.getPublicChatChannelIfExists(channel.idHex)?.notes?.size() ?: 0) > 0) {
|
||||||
|
channel
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sort(items: Set<Note>): List<Note> {
|
||||||
|
val lastNote =
|
||||||
|
items.associateWith { note ->
|
||||||
|
LocalCache.getPublicChatChannelIfExists(note.idHex)?.lastNote?.createdAt() ?: 0L
|
||||||
|
}
|
||||||
|
|
||||||
|
val createdNote =
|
||||||
|
items.associateWith { note ->
|
||||||
|
note.createdAt() ?: 0L
|
||||||
|
}
|
||||||
|
|
||||||
|
val comparator: Comparator<Note> =
|
||||||
|
compareByDescending<Note> {
|
||||||
|
lastNote[it]
|
||||||
|
}.thenByDescending {
|
||||||
|
createdNote[it]
|
||||||
|
}.thenBy {
|
||||||
|
it.idHex
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.sortedWith(comparator)
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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.publicChats.datasource
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
|
class PublicChatsQueryState(
|
||||||
|
val account: Account,
|
||||||
|
val feedStates: AccountFeedContentStates,
|
||||||
|
val scope: CoroutineScope,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class PublicChatsFilterAssembler(
|
||||||
|
client: INostrClient,
|
||||||
|
) : ComposeSubscriptionManager<PublicChatsQueryState>() {
|
||||||
|
val group =
|
||||||
|
listOf(
|
||||||
|
PublicChatsSubAssembler(client, ::allKeys),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun invalidateKeys() = invalidateFilters()
|
||||||
|
|
||||||
|
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
|
||||||
|
|
||||||
|
override fun destroy() = group.forEach { it.destroy() }
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* 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.publicChats.datasource
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PublicChatsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
|
||||||
|
PublicChatsFilterAssemblerSubscription(
|
||||||
|
accountViewModel.dataSources().publicChats,
|
||||||
|
accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PublicChatsFilterAssemblerSubscription(
|
||||||
|
dataSource: PublicChatsFilterAssembler,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
val state =
|
||||||
|
remember(accountViewModel.account) {
|
||||||
|
PublicChatsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyDataSourceSubscription(state, dataSource)
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* 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.publicChats.datasource
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.TopFilter
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.makePublicChatsFilter
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.sample
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class PublicChatsSubAssembler(
|
||||||
|
client: INostrClient,
|
||||||
|
allKeys: () -> Set<PublicChatsQueryState>,
|
||||||
|
) : PerUserAndFollowListEoseManager<PublicChatsQueryState, TopFilter>(client, allKeys) {
|
||||||
|
override fun updateFilter(
|
||||||
|
key: PublicChatsQueryState,
|
||||||
|
since: SincePerRelayMap?,
|
||||||
|
): List<RelayBasedFilter> {
|
||||||
|
val feedSettings = key.followsPerRelay()
|
||||||
|
|
||||||
|
return makePublicChatsFilter(feedSettings, since, key.feedStates.publicChatsFeed.lastNoteCreatedAtIfFilled())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun user(key: PublicChatsQueryState) = key.account.userProfile()
|
||||||
|
|
||||||
|
override fun list(key: PublicChatsQueryState) = key.listName()
|
||||||
|
|
||||||
|
fun PublicChatsQueryState.listNameFlow() = account.settings.defaultPublicChatsFollowList
|
||||||
|
|
||||||
|
fun PublicChatsQueryState.listName() = listNameFlow().value
|
||||||
|
|
||||||
|
fun PublicChatsQueryState.followsPerRelayFlow() = account.livePublicChatsFollowListsPerRelay
|
||||||
|
|
||||||
|
fun PublicChatsQueryState.followsPerRelay() = followsPerRelayFlow().value
|
||||||
|
|
||||||
|
val userJobMap = mutableMapOf<User, List<Job>>()
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
override fun newSub(key: PublicChatsQueryState): Subscription {
|
||||||
|
val user = user(key)
|
||||||
|
userJobMap[user]?.forEach { it.cancel() }
|
||||||
|
userJobMap[user] =
|
||||||
|
listOf(
|
||||||
|
key.scope.launch(Dispatchers.IO) {
|
||||||
|
key.listNameFlow().collectLatest {
|
||||||
|
invalidateFilters()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
key.scope.launch(Dispatchers.IO) {
|
||||||
|
key.followsPerRelayFlow().sample(500).collectLatest {
|
||||||
|
invalidateFilters()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
key.account.scope.launch(Dispatchers.IO) {
|
||||||
|
key.feedStates.publicChatsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
|
||||||
|
invalidateFilters()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return super.newSub(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun endSub(
|
||||||
|
key: User,
|
||||||
|
subId: String,
|
||||||
|
) {
|
||||||
|
super.endSub(key, subId)
|
||||||
|
userJobMap[key]?.forEach { it.cancel() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -485,6 +485,9 @@
|
|||||||
<string name="profile_badges_empty">You haven\'t received any badges yet.</string>
|
<string name="profile_badges_empty">You haven\'t received any badges yet.</string>
|
||||||
<string name="pictures">Pictures</string>
|
<string name="pictures">Pictures</string>
|
||||||
<string name="shorts">Shorts</string>
|
<string name="shorts">Shorts</string>
|
||||||
|
<string name="public_chats">Public Chats</string>
|
||||||
|
<string name="follow_packs">Follow Packs</string>
|
||||||
|
<string name="live_streams">Live Streams</string>
|
||||||
<string name="longs">Videos</string>
|
<string name="longs">Videos</string>
|
||||||
<string name="articles">Articles</string>
|
<string name="articles">Articles</string>
|
||||||
<string name="private_bookmarks">Private Bookmarks</string>
|
<string name="private_bookmarks">Private Bookmarks</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user