- Adds FollowSets to the discovery tabs.

- Refactors discovery screen, creating a package for each feature.
- Fixes Android studio previews
This commit is contained in:
Vitor Pamplona
2025-05-06 13:23:43 -04:00
parent fbe20029f7
commit 80d4971548
127 changed files with 2376 additions and 1503 deletions
@@ -140,7 +140,7 @@ class ThreadDualAxisChartAssemblerTest {
val account = Account(AccountSettings(KeyPair()), scope = CoroutineScope(Dispatchers.IO + SupervisorJob()))
withContext(Dispatchers.Main) {
val user = account.userProfile().live()
val user = account.userProfile().flow()
}
val filter = ThreadFeedFilter(account, naddr.toTag())
@@ -41,6 +41,7 @@ import com.vitorpamplona.amethyst.service.ots.OtsBlockHeightCache
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory
import com.vitorpamplona.amethyst.service.relayClient.CacheClientConnector
import com.vitorpamplona.amethyst.service.relayClient.RelayLogger
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
@@ -109,7 +110,7 @@ class Amethyst : Application() {
// Authenticates with relays.
val authCoordinator = AuthCoordinator(client)
// val logger = if (isDebug) RelayLogger(client) else null
val logger = if (isDebug) RelayLogger(client) else null
// Organizes cache clearing
val trimmingService = MemoryTrimmingService(cache)
@@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun RelaySubscriptionsCoordinatorSubscription() = RelaySubscriptionsCoordinatorSubscription(Amethyst.instance.sources)
fun RelaySubscriptionsCoordinatorSubscription(accountViewModel: AccountViewModel) = RelaySubscriptionsCoordinatorSubscription(accountViewModel.dataSources())
@Composable
fun RelaySubscriptionsCoordinatorSubscription(dataSource: RelaySubscriptionsCoordinator) {
@@ -22,12 +22,15 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun ChannelFinderFilterAssemblerSubscription(channel: Channel) = ChannelFinderFilterAssemblerSubscription(channel, Amethyst.instance.sources.channelFinder)
fun ChannelFinderFilterAssemblerSubscription(
channel: Channel,
accountViewModel: AccountViewModel,
) = ChannelFinderFilterAssemblerSubscription(channel, accountViewModel.dataSources().channelFinder)
@Composable
fun ChannelFinderFilterAssemblerSubscription(
@@ -27,8 +27,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.ChannelState
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.LocalCache.notes
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -41,8 +41,11 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onStart
@Composable
fun observeChannel(baseChannel: Channel): State<ChannelState?> {
ChannelFinderFilterAssemblerSubscription(baseChannel)
fun observeChannel(
baseChannel: Channel,
accountViewModel: AccountViewModel,
): State<ChannelState?> {
ChannelFinderFilterAssemblerSubscription(baseChannel, accountViewModel)
return baseChannel
.flow()
@@ -52,8 +55,11 @@ fun observeChannel(baseChannel: Channel): State<ChannelState?> {
@OptIn(ExperimentalCoroutinesApi::class)
@Composable
fun observeChannelNoteAuthors(baseChannel: Channel): State<ImmutableList<User>> {
ChannelFinderFilterAssemblerSubscription(baseChannel)
fun observeChannelNoteAuthors(
baseChannel: Channel,
accountViewModel: AccountViewModel,
): State<ImmutableList<User>> {
ChannelFinderFilterAssemblerSubscription(baseChannel, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -80,9 +86,12 @@ fun observeChannelNoteAuthors(baseChannel: Channel): State<ImmutableList<User>>
@OptIn(ExperimentalCoroutinesApi::class)
@Composable
fun observeChannelPicture(baseChannel: Channel): State<String?> {
fun observeChannelPicture(
baseChannel: Channel,
accountViewModel: AccountViewModel,
): State<String?> {
// Subscribe in the relay for changes in the metadata of this user.
ChannelFinderFilterAssemblerSubscription(baseChannel)
ChannelFinderFilterAssemblerSubscription(baseChannel, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -99,9 +108,12 @@ fun observeChannelPicture(baseChannel: Channel): State<String?> {
@OptIn(ExperimentalCoroutinesApi::class)
@Composable
fun observeChannelInfo(baseChannel: LiveActivitiesChannel): State<LiveActivitiesEvent?> {
fun observeChannelInfo(
baseChannel: LiveActivitiesChannel,
accountViewModel: AccountViewModel,
): State<LiveActivitiesEvent?> {
// Subscribe in the relay for changes in the metadata of this user.
ChannelFinderFilterAssemblerSubscription(baseChannel)
ChannelFinderFilterAssemblerSubscription(baseChannel, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -22,12 +22,15 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun EventFinderFilterAssemblerSubscription(note: Note) = EventFinderFilterAssemblerSubscription(note, Amethyst.instance.sources.eventFinder)
fun EventFinderFilterAssemblerSubscription(
note: Note,
accountViewModel: AccountViewModel,
) = EventFinderFilterAssemblerSubscription(note, accountViewModel.dataSources().eventFinder)
@Composable
fun EventFinderFilterAssemblerSubscription(
@@ -27,6 +27,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.Event
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -38,9 +39,12 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.sample
@Composable
fun observeNote(note: Note): State<NoteState> {
fun observeNote(
note: Note,
accountViewModel: AccountViewModel,
): State<NoteState> {
// Subscribe in the relay for changes in this note.
EventFinderFilterAssemblerSubscription(note)
EventFinderFilterAssemblerSubscription(note, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
return note
@@ -52,9 +56,12 @@ fun observeNote(note: Note): State<NoteState> {
@Suppress("UNCHECKED_CAST")
@OptIn(ExperimentalCoroutinesApi::class)
@Composable
fun <T : Event> observeNoteEvent(note: Note): State<T?> {
fun <T : Event> observeNoteEvent(
note: Note,
accountViewModel: AccountViewModel,
): State<T?> {
// Subscribe in the relay for changes in this note.
EventFinderFilterAssemblerSubscription(note)
EventFinderFilterAssemblerSubscription(note, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -72,10 +79,11 @@ fun <T : Event> observeNoteEvent(note: Note): State<T?> {
@Composable
fun <T> observeNoteAndMap(
note: Note,
accountViewModel: AccountViewModel,
map: (Note) -> T,
): State<T> {
// Subscribe in the relay for changes in this note.
EventFinderFilterAssemblerSubscription(note)
EventFinderFilterAssemblerSubscription(note, accountViewModel)
val flow =
remember(note) {
@@ -96,10 +104,11 @@ fun <T> observeNoteAndMap(
@Composable
fun <T, U> observeNoteEventAndMap(
note: Note,
accountViewModel: AccountViewModel,
map: (T) -> U,
): State<U?> {
// Subscribe in the relay for changes in this note.
EventFinderFilterAssemblerSubscription(note)
EventFinderFilterAssemblerSubscription(note, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -118,9 +127,12 @@ fun <T, U> observeNoteEventAndMap(
@OptIn(ExperimentalCoroutinesApi::class)
@Composable
fun observeNoteHasEvent(note: Note): State<Boolean> {
fun observeNoteHasEvent(
note: Note,
accountViewModel: AccountViewModel,
): State<Boolean> {
// Subscribe in the relay for changes in this note.
EventFinderFilterAssemblerSubscription(note)
EventFinderFilterAssemblerSubscription(note, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -136,9 +148,12 @@ fun observeNoteHasEvent(note: Note): State<Boolean> {
}
@Composable
fun observeNoteReplies(note: Note): State<NoteState?> {
fun observeNoteReplies(
note: Note,
accountViewModel: AccountViewModel,
): State<NoteState?> {
// Subscribe in the relay for changes in this note.
EventFinderFilterAssemblerSubscription(note)
EventFinderFilterAssemblerSubscription(note, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
return note
@@ -149,9 +164,12 @@ fun observeNoteReplies(note: Note): State<NoteState?> {
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@Composable
fun observeNoteReplyCount(note: Note): State<Int> {
fun observeNoteReplyCount(
note: Note,
accountViewModel: AccountViewModel,
): State<Int> {
// Subscribe in the relay for changes in this note.
EventFinderFilterAssemblerSubscription(note)
EventFinderFilterAssemblerSubscription(note, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -168,9 +186,12 @@ fun observeNoteReplyCount(note: Note): State<Int> {
}
@Composable
fun observeNoteReactions(note: Note): State<NoteState?> {
fun observeNoteReactions(
note: Note,
accountViewModel: AccountViewModel,
): State<NoteState?> {
// Subscribe in the relay for changes in this note.
EventFinderFilterAssemblerSubscription(note)
EventFinderFilterAssemblerSubscription(note, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
return note
@@ -181,9 +202,12 @@ fun observeNoteReactions(note: Note): State<NoteState?> {
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@Composable
fun observeNoteReactionCount(note: Note): State<Int> {
fun observeNoteReactionCount(
note: Note,
accountViewModel: AccountViewModel,
): State<Int> {
// Subscribe in the relay for changes in this note.
EventFinderFilterAssemblerSubscription(note)
EventFinderFilterAssemblerSubscription(note, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -202,9 +226,12 @@ fun observeNoteReactionCount(note: Note): State<Int> {
}
@Composable
fun observeNoteZaps(note: Note): State<NoteState?> {
fun observeNoteZaps(
note: Note,
accountViewModel: AccountViewModel,
): State<NoteState?> {
// Subscribe in the relay for changes in this note.
EventFinderFilterAssemblerSubscription(note)
EventFinderFilterAssemblerSubscription(note, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
return note
@@ -214,9 +241,12 @@ fun observeNoteZaps(note: Note): State<NoteState?> {
}
@Composable
fun observeNoteReposts(note: Note): State<NoteState?> {
fun observeNoteReposts(
note: Note,
accountViewModel: AccountViewModel,
): State<NoteState?> {
// Subscribe in the relay for changes in this note.
EventFinderFilterAssemblerSubscription(note)
EventFinderFilterAssemblerSubscription(note, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
return note
@@ -230,9 +260,10 @@ fun observeNoteReposts(note: Note): State<NoteState?> {
fun observeNoteRepostsBy(
note: Note,
user: User,
accountViewModel: AccountViewModel,
): State<Boolean> {
// Subscribe in the relay for changes in this note.
EventFinderFilterAssemblerSubscription(note)
EventFinderFilterAssemblerSubscription(note, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -250,9 +281,12 @@ fun observeNoteRepostsBy(
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@Composable
fun observeNoteRepostCount(note: Note): State<Int> {
fun observeNoteRepostCount(
note: Note,
accountViewModel: AccountViewModel,
): State<Int> {
// Subscribe in the relay for changes in this note.
EventFinderFilterAssemblerSubscription(note)
EventFinderFilterAssemblerSubscription(note, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -269,9 +303,12 @@ fun observeNoteRepostCount(note: Note): State<Int> {
}
@Composable
fun observeNoteReferences(note: Note): State<Boolean> {
fun observeNoteReferences(
note: Note,
accountViewModel: AccountViewModel,
): State<Boolean> {
// Subscribe in the relay for changes in this note.
EventFinderFilterAssemblerSubscription(note)
EventFinderFilterAssemblerSubscription(note, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -289,9 +326,12 @@ fun observeNoteReferences(note: Note): State<Boolean> {
}
@Composable
fun observeNoteOts(note: Note): State<NoteState?> {
fun observeNoteOts(
note: Note,
accountViewModel: AccountViewModel,
): State<NoteState?> {
// Subscribe in the relay for changes in this note.
EventFinderFilterAssemblerSubscription(note)
EventFinderFilterAssemblerSubscription(note, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
return note
@@ -302,9 +342,12 @@ fun observeNoteOts(note: Note): State<NoteState?> {
}
@Composable
fun observeNoteEdits(note: Note): State<NoteState?> {
fun observeNoteEdits(
note: Note,
accountViewModel: AccountViewModel,
): State<NoteState?> {
// Subscribe in the relay for changes in this note.
EventFinderFilterAssemblerSubscription(note)
EventFinderFilterAssemblerSubscription(note, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
return note
@@ -22,12 +22,15 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun UserFinderFilterAssemblerSubscription(user: User) = UserFinderFilterAssemblerSubscription(user, Amethyst.instance.sources.userFinder)
fun UserFinderFilterAssemblerSubscription(
user: User,
accountViewModel: AccountViewModel,
) = UserFinderFilterAssemblerSubscription(user, accountViewModel.dataSources().userFinder)
@Composable
fun UserFinderFilterAssemblerSubscription(
@@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
@@ -48,9 +49,12 @@ import kotlinx.coroutines.flow.sample
import java.math.BigDecimal
@Composable
fun observeUser(user: User): State<UserState?> {
fun observeUser(
user: User,
accountViewModel: AccountViewModel,
): State<UserState?> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
return user
@@ -61,9 +65,12 @@ fun observeUser(user: User): State<UserState?> {
@OptIn(ExperimentalCoroutinesApi::class)
@Composable
fun observeUserName(user: User): State<String> {
fun observeUserName(
user: User,
accountViewModel: AccountViewModel,
): State<String> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
val flow =
remember(user) {
@@ -80,9 +87,12 @@ fun observeUserName(user: User): State<String> {
@OptIn(ExperimentalCoroutinesApi::class)
@Composable
fun observeUserNip05(user: User): State<String?> {
fun observeUserNip05(
user: User,
accountViewModel: AccountViewModel,
): State<String?> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
val flow =
remember(user) {
@@ -99,9 +109,12 @@ fun observeUserNip05(user: User): State<String?> {
@OptIn(ExperimentalCoroutinesApi::class)
@Composable
fun observeUserAboutMe(user: User): State<String> {
fun observeUserAboutMe(
user: User,
accountViewModel: AccountViewModel,
): State<String> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -118,9 +131,12 @@ fun observeUserAboutMe(user: User): State<String> {
@OptIn(ExperimentalCoroutinesApi::class)
@Composable
fun observeUserInfo(user: User): State<UserMetadata?> {
fun observeUserInfo(
user: User,
accountViewModel: AccountViewModel,
): State<UserMetadata?> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -137,9 +153,12 @@ fun observeUserInfo(user: User): State<UserMetadata?> {
@OptIn(ExperimentalCoroutinesApi::class)
@Composable
fun observeUserBanner(user: User): State<String?> {
fun observeUserBanner(
user: User,
accountViewModel: AccountViewModel,
): State<String?> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -156,9 +175,12 @@ fun observeUserBanner(user: User): State<String?> {
@OptIn(ExperimentalCoroutinesApi::class)
@Composable
fun observeUserPicture(user: User): State<String?> {
fun observeUserPicture(
user: User,
accountViewModel: AccountViewModel,
): State<String?> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -175,9 +197,12 @@ fun observeUserPicture(user: User): State<String?> {
@OptIn(ExperimentalCoroutinesApi::class)
@Composable
fun observeUserShortName(user: User): State<String> {
fun observeUserShortName(
user: User,
accountViewModel: AccountViewModel,
): State<String> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -193,9 +218,12 @@ fun observeUserShortName(user: User): State<String> {
}
@Composable
fun observeUserFollows(user: User): State<UserState?> {
fun observeUserFollows(
user: User,
accountViewModel: AccountViewModel,
): State<UserState?> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
return user
@@ -206,9 +234,12 @@ fun observeUserFollows(user: User): State<UserState?> {
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@Composable
fun observeUserFollowCount(user: User): State<Int> {
fun observeUserFollowCount(
user: User,
accountViewModel: AccountViewModel,
): State<Int> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -228,9 +259,12 @@ fun observeUserFollowCount(user: User): State<Int> {
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@Composable
fun observeUserTagFollows(user: User): State<Int> {
fun observeUserTagFollows(
user: User,
accountViewModel: AccountViewModel,
): State<Int> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -249,9 +283,12 @@ fun observeUserTagFollows(user: User): State<Int> {
}
@Composable
fun observeUserBookmarks(user: User): State<UserState?> {
fun observeUserBookmarks(
user: User,
accountViewModel: AccountViewModel,
): State<UserState?> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
return user
@@ -262,9 +299,12 @@ fun observeUserBookmarks(user: User): State<UserState?> {
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@Composable
fun observeUserBookmarkCount(user: User): State<Int> {
fun observeUserBookmarkCount(
user: User,
accountViewModel: AccountViewModel,
): State<Int> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -283,9 +323,12 @@ fun observeUserBookmarkCount(user: User): State<Int> {
}
@Composable
fun observeUserFollowers(user: User): State<UserState?> {
fun observeUserFollowers(
user: User,
accountViewModel: AccountViewModel,
): State<UserState?> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
return user
@@ -296,9 +339,12 @@ fun observeUserFollowers(user: User): State<UserState?> {
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@Composable
fun observeUserFollowerCount(user: User): State<Int> {
fun observeUserFollowerCount(
user: User,
accountViewModel: AccountViewModel,
): State<Int> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -321,9 +367,10 @@ fun observeUserFollowerCount(user: User): State<Int> {
fun observeUserIsFollowing(
user1: User,
user2: User,
accountViewModel: AccountViewModel,
): State<Boolean> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user1)
UserFinderFilterAssemblerSubscription(user1, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -346,9 +393,10 @@ fun observeUserIsFollowing(
fun observeUserIsFollowingHashtag(
user: User,
hashtag: String,
accountViewModel: AccountViewModel,
): State<Boolean> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -371,9 +419,10 @@ fun observeUserIsFollowingHashtag(
fun observeUserIsFollowingGeohash(
user: User,
geohash: String,
accountViewModel: AccountViewModel,
): State<Boolean> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -396,9 +445,10 @@ fun observeUserIsFollowingGeohash(
fun observeUserIsFollowingChannel(
account: Account,
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
): State<Boolean> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(account.userProfile())
UserFinderFilterAssemblerSubscription(account.userProfile(), accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -421,9 +471,10 @@ fun observeUserIsFollowingChannel(
fun observeUserIsFollowingChannel(
account: Account,
channel: EphemeralChatChannel,
accountViewModel: AccountViewModel,
): State<Boolean> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(account.userProfile())
UserFinderFilterAssemblerSubscription(account.userProfile(), accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -442,9 +493,12 @@ fun observeUserIsFollowingChannel(
}
@Composable
fun observeUserZaps(user: User): State<UserState?> {
fun observeUserZaps(
user: User,
accountViewModel: AccountViewModel,
): State<UserState?> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
return user
@@ -455,9 +509,12 @@ fun observeUserZaps(user: User): State<UserState?> {
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@Composable
fun observeUserZapAmount(user: User): State<BigDecimal> {
fun observeUserZapAmount(
user: User,
accountViewModel: AccountViewModel,
): State<BigDecimal> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -476,9 +533,12 @@ fun observeUserZapAmount(user: User): State<BigDecimal> {
}
@Composable
fun observeUserReports(user: User): State<UserState?> {
fun observeUserReports(
user: User,
accountViewModel: AccountViewModel,
): State<UserState?> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
return user
@@ -489,9 +549,12 @@ fun observeUserReports(user: User): State<UserState?> {
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@Composable
fun observeUserReportCount(user: User): State<Int> {
fun observeUserReportCount(
user: User,
accountViewModel: AccountViewModel,
): State<Int> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -512,9 +575,12 @@ fun observeUserReportCount(user: User): State<Int> {
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@Composable
fun observeUserStatuses(user: User): State<ImmutableList<AddressableNote>> {
fun observeUserStatuses(
user: User,
accountViewModel: AccountViewModel,
): State<ImmutableList<AddressableNote>> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -538,9 +604,10 @@ fun observeUserStatuses(user: User): State<ImmutableList<AddressableNote>> {
fun observeUserRelayIntoList(
user: User,
relayUrl: String,
accountViewModel: AccountViewModel,
): State<Boolean> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -566,9 +633,10 @@ fun observeUserRelayIntoList(
fun observeUserRoomSubject(
user: User,
room: ChatroomKey,
accountViewModel: AccountViewModel,
): State<String?> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -594,9 +662,12 @@ data class RelayUsage(
@OptIn(FlowPreview::class)
@Composable
fun observeUserRelaysUsing(user: User): State<RelayUsage> {
fun observeUserRelaysUsing(
user: User,
accountViewModel: AccountViewModel,
): State<RelayUsage> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
UserFinderFilterAssemblerSubscription(user, accountViewModel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -21,12 +21,15 @@
package com.vitorpamplona.amethyst.service.relayClient.searchCommand
import androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchBarViewModel
@Composable
fun TextSearchDataSourceSubscription(searchBarViewModel: SearchBarViewModel) = TextSearchDataSourceSubscription(searchBarViewModel, Amethyst.instance.sources.search)
fun TextSearchDataSourceSubscription(
searchBarViewModel: SearchBarViewModel,
accountViewModel: AccountViewModel,
) = TextSearchDataSourceSubscription(searchBarViewModel, accountViewModel.dataSources().search)
@Composable
fun TextSearchDataSourceSubscription(
@@ -21,12 +21,15 @@
package com.vitorpamplona.amethyst.service.relayClient.searchCommand
import androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun UserSearchDataSourceSubscription(userSuggestions: UserSuggestionState) = UserSearchDataSourceSubscription(userSuggestions, Amethyst.instance.sources.search)
fun UserSearchDataSourceSubscription(
userSuggestions: UserSuggestionState,
accountViewModel: AccountViewModel,
) = UserSearchDataSourceSubscription(userSuggestions, accountViewModel.dataSources().search)
@Composable
fun UserSearchDataSourceSubscription(
@@ -192,7 +192,7 @@ private fun DisplayNoteLink(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteState by observeNote(it)
val noteState by observeNote(it, accountViewModel)
val note = remember(noteState) { noteState?.note } ?: return
val channelHex = remember(noteState) { note.channelHex() }
@@ -215,7 +215,7 @@ private fun DisplayNoteLink(
)
} else if (channelHex != null) {
LoadChannel(baseChannelHex = channelHex, accountViewModel) { baseChannel ->
val channelState by observeChannel(baseChannel)
val channelState by observeChannel(baseChannel, accountViewModel)
val channelDisplayName by
remember(channelState) {
derivedStateOf { channelState?.channel?.toBestDisplayName() ?: noteIdDisplayNote }
@@ -255,7 +255,7 @@ private fun DisplayAddress(
}
noteBase?.let {
val noteState by observeNote(it)
val noteState by observeNote(it, accountViewModel)
val route = remember(noteState) { Route.Note(nip19.aTag()) }
val displayName = remember(noteState) { "@${noteState?.note?.idDisplayNote()}" }
@@ -303,7 +303,7 @@ fun DisplayUser(
}
}
userBase?.let { RenderUserAsClickableText(it, additionalChars, nav) }
userBase?.let { RenderUserAsClickableText(it, additionalChars, accountViewModel, nav) }
if (userBase == null) {
val uri = LocalUriHandler.current
@@ -323,9 +323,10 @@ fun DisplayUser(
fun RenderUserAsClickableText(
baseUser: User,
additionalChars: String?,
accountViewModel: AccountViewModel,
nav: INav,
) {
val userState by observeUserInfo(baseUser)
val userState by observeUserInfo(baseUser, accountViewModel)
CreateClickableTextWithEmoji(
clickablePart = "@" + (userState?.bestName() ?: baseUser.pubkeyDisplayHex()),
@@ -777,7 +777,7 @@ private fun DisplayUserFromTag(
accountViewModel: AccountViewModel,
nav: INav,
) {
val meta by observeUserInfo(baseUser)
val meta by observeUserInfo(baseUser, accountViewModel)
CrossfadeIfEnabled(targetState = meta, label = "DisplayUserFromTag", accountViewModel = accountViewModel) {
Row {
@@ -227,7 +227,7 @@ class MarkdownMediaRenderer(
) {
renderInvisible(richTextStringBuilder) {
// Preloads note if not loaded yet.
EventFinderFilterAssemblerSubscription(baseNote)
EventFinderFilterAssemblerSubscription(baseNote, accountViewModel)
}
}
@@ -45,6 +45,7 @@ object ScrollStateKeys {
const val DRAFTS = "DraftsFeed"
const val DISCOVER_FOLLOWS = "DiscoverFollowSetsFeed"
const val DISCOVER_CONTENT = "DiscoverDiscoverContentFeed"
const val DISCOVER_MARKETPLACE = "DiscoverMarketplaceFeed"
const val DISCOVER_LIVE = "DiscoverLiveFeed"
@@ -173,14 +173,10 @@ fun DisplayAccount(
.width(55.dp)
.padding(0.dp),
) {
AccountPicture(
it,
accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
)
AccountPicture(it, accountViewModel)
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) { AccountName(acc, it) }
Column(modifier = Modifier.weight(1f)) { AccountName(acc, it, accountViewModel) }
Column(modifier = Modifier.width(32.dp)) { ActiveMarker(acc, accountViewModel) }
}
}
@@ -212,18 +208,17 @@ private fun ActiveMarker(
@Composable
private fun AccountPicture(
user: User,
loadProfilePicture: Boolean,
loadRobohash: Boolean,
accountViewModel: AccountViewModel,
) {
val profilePicture by observeUserPicture(user)
val profilePicture by observeUserPicture(user, accountViewModel)
RobohashFallbackAsyncImage(
robot = user.pubkeyHex,
model = profilePicture,
contentDescription = stringRes(R.string.profile_image),
modifier = AccountPictureModifier,
loadProfilePicture = loadProfilePicture,
loadRobohash = loadRobohash,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
)
}
@@ -231,8 +226,9 @@ private fun AccountPicture(
private fun AccountName(
acc: AccountInfo,
user: User,
accountViewModel: AccountViewModel,
) {
val info by observeUserInfo(user)
val info by observeUserInfo(user, accountViewModel)
info?.let {
it.bestName()?.let { name ->
@@ -66,7 +66,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28P
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.MessagesScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.CommunityScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.marketplace.NewProductScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.NewProductScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.DraftListScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.DvmContentDiscoveryScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashScreen
@@ -89,7 +89,7 @@ private fun LoggedInUserPictureDrawer(
onClick: () -> Unit,
) {
IconButton(onClick = onClick) {
val profilePicture by observeUserPicture(accountViewModel.userProfile())
val profilePicture by observeUserPicture(accountViewModel.userProfile(), accountViewModel)
RobohashFallbackAsyncImage(
robot = accountViewModel.userProfile().pubkeyHex,
@@ -107,6 +107,7 @@ private fun LoggedInUserPictureDrawer(
fun FollowListWithRoutes(
followListsModel: FollowListState,
listName: String,
accountViewModel: AccountViewModel,
onChange: (FeedDefinition) -> Unit,
) {
val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsStateWithLifecycle()
@@ -116,6 +117,7 @@ fun FollowListWithRoutes(
explainer = stringRes(R.string.select_list_to_filter),
options = allLists,
onSelect = { onChange(allLists.getOrNull(it) ?: followListsModel.kind3Follow) },
accountViewModel = accountViewModel,
)
}
@@ -123,6 +125,7 @@ fun FollowListWithRoutes(
fun FollowListWithoutRoutes(
followListsModel: FollowListState,
listName: String,
accountViewModel: AccountViewModel,
onChange: (FeedDefinition) -> Unit,
) {
val allLists by followListsModel.kind3GlobalPeople.collectAsStateWithLifecycle()
@@ -132,5 +135,6 @@ fun FollowListWithoutRoutes(
explainer = stringRes(R.string.select_list_to_filter),
options = allLists,
onSelect = { onChange(allLists.getOrNull(it) ?: followListsModel.kind3Follow) },
accountViewModel = accountViewModel,
)
}
@@ -157,7 +157,7 @@ fun DrawerContent(
EditStatusBoxes(accountViewModel.account.userProfile(), accountViewModel, nav)
}
FollowingAndFollowerCounts(accountViewModel.account, onClickUser)
FollowingAndFollowerCounts(accountViewModel.account, accountViewModel, onClickUser)
HorizontalDivider(
thickness = DividerThickness,
@@ -191,7 +191,7 @@ fun ProfileContent(
accountViewModel: AccountViewModel,
onClickUser: () -> Unit,
) {
val userInfo by observeUserInfo(baseAccountUser)
val userInfo by observeUserInfo(baseAccountUser, accountViewModel)
ProfileContentTemplate(
profilePubHex = baseAccountUser.pubkeyHex,
@@ -278,7 +278,7 @@ private fun EditStatusBoxes(
StatusEditBar(accountViewModel = accountViewModel, nav = nav)
} else {
statuses.forEach {
val noteStatus by observeNote(it)
val noteStatus by observeNote(it, accountViewModel)
StatusEditBar(noteStatus.note.event?.content, it.address, accountViewModel, nav)
}
@@ -387,13 +387,14 @@ fun UserStatusDeleteButton(onClick: () -> Unit) {
@Composable
private fun FollowingAndFollowerCounts(
baseAccountUser: Account,
accountViewModel: AccountViewModel,
onClick: () -> Unit,
) {
Row(
modifier = drawerSpacing.clickable(onClick = onClick),
) {
val followingCount = baseAccountUser.liveKind3Follows.collectAsStateWithLifecycle()
val followerCount by observeUserFollowerCount(baseAccountUser.userProfile())
val followerCount by observeUserFollowerCount(baseAccountUser.userProfile(), accountViewModel)
Text(
text =
@@ -65,6 +65,7 @@ import com.vitorpamplona.amethyst.ui.screen.HashtagName
import com.vitorpamplona.amethyst.ui.screen.Name
import com.vitorpamplona.amethyst.ui.screen.PeopleListName
import com.vitorpamplona.amethyst.ui.screen.ResourceName
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
@@ -82,6 +83,7 @@ fun FeedFilterSpinner(
options: ImmutableList<FeedDefinition>,
onSelect: (Int) -> Unit,
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
) {
var optionsShowing by remember { mutableStateOf(false) }
@@ -208,14 +210,17 @@ fun FeedFilterSpinner(
onSelect(it)
},
) {
RenderOption(it.name)
RenderOption(it.name, accountViewModel)
}
}
}
}
@Composable
fun RenderOption(option: Name) {
fun RenderOption(
option: Name,
accountViewModel: AccountViewModel,
) {
when (option) {
is GeoHashName -> {
LoadCityName(option.geoHashTag) {
@@ -251,7 +256,7 @@ fun RenderOption(option: Name) {
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth(),
) {
val noteState by observeNote(option.note)
val noteState by observeNote(option.note, accountViewModel)
val noteEvent = noteState.note.event
val name =
@@ -271,7 +276,7 @@ fun RenderOption(option: Name) {
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth(),
) {
val it by observeNote(option.note)
val it by observeNote(option.note, accountViewModel)
Text(text = "/n/${((it?.note as? AddressableNote)?.dTag() ?: "")}", color = MaterialTheme.colorScheme.onSurface)
}
@@ -61,7 +61,7 @@ fun BadgeCompose(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteState by observeNote(likeSetCard.note)
val noteState by observeNote(likeSetCard.note, accountViewModel)
val note = noteState?.note
val context = LocalContext.current.applicationContext
@@ -129,7 +129,7 @@ fun BadgeCompose(
}
note.replyTo?.firstOrNull()?.let {
BadgeDisplay(baseNote = it)
BadgeDisplay(baseNote = it, accountViewModel)
}
}
}
@@ -147,8 +147,8 @@ fun HiddenNote(
NoteAuthorPicture(
baseNote = it,
size = Size35dp,
nav = nav,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,46 @@
/**
* Copyright (c) 2024 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.note
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
@Composable
fun DisplayAuthorBanner(
note: Note,
accountViewModel: AccountViewModel,
) {
WatchAuthor(note, accountViewModel) {
BannerImage(
it,
Modifier
.fillMaxSize()
.clip(QuoteBorder),
accountViewModel,
)
}
}
@@ -0,0 +1,84 @@
/**
* Copyright (c) 2024 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.note
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.nav
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Size25dp
import kotlinx.collections.immutable.ImmutableList
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun Gallery(
users: ImmutableList<User>,
modifier: Modifier,
accountViewModel: AccountViewModel,
nav: INav,
maxPictures: Int = 6,
) {
FlowRow(
modifier,
verticalArrangement = Arrangement.Center,
horizontalArrangement = Arrangement.spacedBy((-5).dp),
) {
users.take(maxPictures).forEach {
ClickableUserPicture(
it,
Size25dp,
accountViewModel,
onClick = {
nav.nav(routeFor(it))
},
)
}
if (users.size > maxPictures) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.size(Size25dp).clip(shape = CircleShape).background(MaterialTheme.colorScheme.secondaryContainer),
) {
Text(
text = "+" + showCount(users.size - maxPictures),
fontSize = 10.sp,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
}
}
@@ -144,7 +144,7 @@ fun LoadStatuses(
accountViewModel: AccountViewModel,
content: @Composable (ImmutableList<AddressableNote>) -> Unit,
) {
val userStatuses by observeUserStatuses(user)
val userStatuses by observeUserStatuses(user, accountViewModel)
content(userStatuses)
}
@@ -158,7 +158,7 @@ fun LoadOts(
) {
var earliestDate: GenericLoadable<Long> by remember { mutableStateOf(GenericLoadable.Loading()) }
val noteStatus by observeNoteOts(note)
val noteStatus by observeNoteOts(note, accountViewModel)
LaunchedEffect(key1 = noteStatus) {
accountViewModel.findOtsEventsForNote(noteStatus?.note ?: note) { newOts ->
@@ -597,7 +597,7 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture(
author: User,
accountViewModel: AccountViewModel,
) {
WatchUserMetadata(author) { baseUserPicture ->
WatchUserMetadata(author, accountViewModel) { baseUserPicture ->
RobohashFallbackAsyncImage(
robot = author.pubkeyHex,
model = baseUserPicture,
@@ -621,9 +621,10 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture(
@Composable
private fun WatchUserMetadata(
author: User,
accountViewModel: AccountViewModel,
onNewMetadata: @Composable (String?) -> Unit,
) {
val userProfile by observeUserPicture(author)
val userProfile by observeUserPicture(author, accountViewModel)
onNewMetadata(userProfile)
}
@@ -123,7 +123,7 @@ fun ObserveDisplayNip05Status(
accountViewModel: AccountViewModel,
nav: INav,
) {
WatchAuthor(baseNote = baseNote) {
WatchAuthor(baseNote = baseNote, accountViewModel) {
ObserveDisplayNip05Status(it, columnModifier, accountViewModel, nav)
}
}
@@ -135,8 +135,8 @@ fun ObserveDisplayNip05Status(
accountViewModel: AccountViewModel,
nav: INav,
) {
val nip05 by observeUserNip05(baseUser)
val statuses by observeUserStatuses(baseUser)
val nip05 by observeUserNip05(baseUser, accountViewModel)
val statuses by observeUserStatuses(baseUser, accountViewModel)
CrossfadeIfEnabled(
targetState = nip05,
@@ -193,7 +193,7 @@ fun ObserveRotateStatuses(
accountViewModel: AccountViewModel,
nav: INav,
) {
ObserveAllStatusesToAvoidSwitchigAllTheTime(statuses)
ObserveAllStatusesToAvoidSwitchigAllTheTime(statuses, accountViewModel)
RotateStatuses(
statuses,
@@ -203,9 +203,12 @@ fun ObserveRotateStatuses(
}
@Composable
fun ObserveAllStatusesToAvoidSwitchigAllTheTime(statuses: ImmutableList<AddressableNote>) {
fun ObserveAllStatusesToAvoidSwitchigAllTheTime(
statuses: ImmutableList<AddressableNote>,
accountViewModel: AccountViewModel,
) {
statuses.map {
EventFinderFilterAssemblerSubscription(it)
EventFinderFilterAssemblerSubscription(it, accountViewModel)
}
}
@@ -246,7 +249,7 @@ fun DisplayStatus(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteState by observeNote(addressableNote)
val noteState by observeNote(addressableNote, accountViewModel)
val noteEvent = noteState?.note?.event as? StatusEvent ?: return
DisplayStatus(noteEvent, accountViewModel, nav)
@@ -291,7 +291,7 @@ fun AcceptableNote(
nav = nav,
)
}
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote, accountViewModel)
else ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
CheckNewAndRenderNote(
@@ -331,7 +331,7 @@ fun AcceptableNote(
nav = nav,
)
}
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote, accountViewModel)
else ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
CheckNewAndRenderNote(
@@ -856,7 +856,7 @@ fun ObserveDraftEvent(
accountViewModel: AccountViewModel,
render: @Composable (Note) -> Unit,
) {
val noteEvent by observeNoteEvent<DraftEvent>(note)
val noteEvent by observeNoteEvent<DraftEvent>(note, accountViewModel)
noteEvent?.let {
val innerNote by produceCachedStateAsync(cache = accountViewModel.draftNoteCache, key = it)
@@ -1051,7 +1051,7 @@ fun FirstUserInfoRow(
val textColor = if (isRepost) MaterialTheme.colorScheme.grayText else Color.Unspecified
if (showAuthorPicture) {
NoteAuthorPicture(baseNote, nav, accountViewModel, Size25dp)
NoteAuthorPicture(baseNote, Size25dp, accountViewModel = accountViewModel, nav = nav)
Spacer(HalfPadding)
NoteUsernameDisplay(baseNote, Modifier.weight(1f), textColor = textColor, accountViewModel = accountViewModel)
} else {
@@ -1116,7 +1116,7 @@ fun observeEdits(
)
}
val updatedNote by observeNoteEdits(baseNote)
val updatedNote by observeNoteEdits(baseNote, accountViewModel)
LaunchedEffect(key1 = updatedNote) {
updatedNote?.note?.let {
@@ -1167,10 +1167,10 @@ private fun RenderAuthorImages(
if (baseRepost != null) {
RepostNoteAuthorPicture(baseNote, baseRepost, accountViewModel, nav)
} else {
NoteAuthorPicture(baseNote, nav, accountViewModel, Size55dp)
NoteAuthorPicture(baseNote, Size55dp, accountViewModel = accountViewModel, nav = nav)
}
} else {
NoteAuthorPicture(baseNote, nav, accountViewModel, Size55dp)
NoteAuthorPicture(baseNote, Size55dp, accountViewModel = accountViewModel, nav = nav)
}
if (baseNote.event is ChannelMessageEvent) {
@@ -1179,8 +1179,7 @@ private fun RenderAuthorImages(
LoadChannel(baseChannelHex, accountViewModel) { channel ->
ChannelNotePicture(
channel,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
accountViewModel,
)
}
}
@@ -1190,18 +1189,17 @@ private fun RenderAuthorImages(
@Composable
private fun ChannelNotePicture(
baseChannel: Channel,
loadProfilePicture: Boolean,
loadRobohash: Boolean,
accountViewModel: AccountViewModel,
) {
val model by observeChannelPicture(baseChannel)
val model by observeChannelPicture(baseChannel, accountViewModel)
RobohashFallbackAsyncImage(
robot = baseChannel.idHex,
model = model,
contentDescription = stringRes(R.string.group_picture),
modifier = MaterialTheme.colorScheme.channelNotePictureModifier,
loadProfilePicture = loadProfilePicture,
loadRobohash = loadRobohash,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
)
}
@@ -1216,17 +1214,17 @@ private fun RepostNoteAuthorPicture(
baseAuthorPicture = {
NoteAuthorPicture(
baseNote = baseNote,
nav = nav,
accountViewModel = accountViewModel,
size = Size34dp,
accountViewModel = accountViewModel,
nav = nav,
)
},
repostAuthorPicture = {
NoteAuthorPicture(
baseNote = baseRepost,
nav = nav,
accountViewModel = accountViewModel,
size = Size34dp,
accountViewModel = accountViewModel,
nav = nav,
)
},
)
@@ -264,7 +264,7 @@ fun PollNote(
accountViewModel: AccountViewModel,
nav: INav,
) {
WatchZapsAndUpdateTallies(baseNote, pollViewModel)
WatchZapsAndUpdateTallies(baseNote, pollViewModel, accountViewModel)
pollViewModel.tallies.forEach { option ->
OptionNote(
@@ -283,8 +283,9 @@ fun PollNote(
private fun WatchZapsAndUpdateTallies(
baseNote: Note,
pollViewModel: PollNoteViewModel,
accountViewModel: AccountViewModel,
) {
val zapsState by observeNoteZaps(baseNote)
val zapsState by observeNoteZaps(baseNote, accountViewModel)
LaunchedEffect(key1 = zapsState) { pollViewModel.refreshTallies() }
}
@@ -202,7 +202,7 @@ private fun InnerReactionRow(
showReactionDetail = showReactionDetail,
addPadding = addPadding,
one = {
WatchReactionsZapsBoostsAndDisplayIfExists(baseNote) {
WatchReactionsZapsBoostsAndDisplayIfExists(baseNote, accountViewModel) {
RenderShowIndividualReactionsButton(wantsToSeeReactions, accountViewModel)
}
},
@@ -345,7 +345,7 @@ fun RenderZapRaiser(
details: Boolean,
accountViewModel: AccountViewModel,
) {
val zapsState by observeNoteZaps(baseNote)
val zapsState by observeNoteZaps(baseNote, accountViewModel)
var zapraiserStatus by remember { mutableStateOf(ZapraiserStatus(0F, "$zapraiserAmount")) }
@@ -393,9 +393,10 @@ fun RenderZapRaiser(
@Composable
private fun WatchReactionsZapsBoostsAndDisplayIfExists(
baseNote: Note,
accountViewModel: AccountViewModel,
content: @Composable () -> Unit,
) {
val hasReactions by observeNoteReferences(baseNote)
val hasReactions by observeNoteReferences(baseNote, accountViewModel)
if (hasReactions) {
content()
@@ -434,7 +435,7 @@ private fun ReactionDetailGallery(
val defaultBackgroundColor = MaterialTheme.colorScheme.background
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
val hasReactions by observeNoteReferences(baseNote)
val hasReactions by observeNoteReferences(baseNote, accountViewModel)
if (hasReactions) {
Row(
@@ -456,7 +457,7 @@ private fun WatchBoostsAndRenderGallery(
nav: INav,
accountViewModel: AccountViewModel,
) {
val boostsEvents by observeNoteReposts(baseNote)
val boostsEvents by observeNoteReposts(baseNote, accountViewModel)
boostsEvents?.let {
if (it.note.boosts.isNotEmpty()) {
@@ -475,7 +476,7 @@ private fun WatchReactionsAndRenderGallery(
nav: INav,
accountViewModel: AccountViewModel,
) {
val reactionsState by observeNoteReactions(baseNote)
val reactionsState by observeNoteReactions(baseNote, accountViewModel)
val reactionEvents = reactionsState?.note?.reactions ?: return
if (reactionEvents.isNotEmpty()) {
@@ -498,7 +499,7 @@ private fun WatchZapAndRenderGallery(
nav: INav,
accountViewModel: AccountViewModel,
) {
val zapsState by observeNoteZaps(baseNote)
val zapsState by observeNoteZaps(baseNote, accountViewModel)
var zapEvents by
remember(zapsState) {
@@ -655,7 +656,7 @@ fun ReplyCounter(
textColor: Color,
accountViewModel: AccountViewModel,
) {
val repliesState by observeNoteReplyCount(baseNote)
val repliesState by observeNoteReplyCount(baseNote, accountViewModel)
SlidingAnimationCount(repliesState, textColor, accountViewModel)
}
@@ -790,7 +791,7 @@ fun ObserveBoostIcon(
accountViewModel: AccountViewModel,
inner: @Composable (Boolean) -> Unit,
) {
val hasBoosted by observeNoteRepostsBy(baseNote, accountViewModel.userProfile())
val hasBoosted by observeNoteRepostsBy(baseNote, accountViewModel.userProfile(), accountViewModel)
inner(hasBoosted)
}
@@ -801,7 +802,7 @@ fun BoostText(
grayTint: Color,
accountViewModel: AccountViewModel,
) {
val boostState by observeNoteRepostCount(baseNote)
val boostState by observeNoteRepostCount(baseNote, accountViewModel)
SlidingAnimationCount(boostState, grayTint, accountViewModel)
}
@@ -862,7 +863,7 @@ fun LikeReaction(
}
}
ObserveLikeText(baseNote) { reactionCount -> SlidingAnimationCount(reactionCount, grayTint, accountViewModel) }
ObserveLikeText(baseNote, accountViewModel) { reactionCount -> SlidingAnimationCount(reactionCount, grayTint, accountViewModel) }
}
@Composable
@@ -871,7 +872,7 @@ fun ObserveLikeIcon(
accountViewModel: AccountViewModel,
inner: @Composable (String?) -> Unit,
) {
val reactionsState by observeNoteReactions(baseNote)
val reactionsState by observeNoteReactions(baseNote, accountViewModel)
@Suppress("ProduceStateDoesNotAssignValue")
val reactionType by
@@ -926,9 +927,10 @@ private fun RenderReactionType(
@Composable
fun ObserveLikeText(
baseNote: Note,
accountViewModel: AccountViewModel,
inner: @Composable (Int) -> Unit,
) {
val reactionCount by observeNoteReactionCount(baseNote)
val reactionCount by observeNoteReactionCount(baseNote, accountViewModel)
inner(reactionCount)
}
@@ -1182,7 +1184,7 @@ fun ObserveZapIcon(
val wasZappedByLoggedInUser = remember { mutableStateOf(false) }
if (!wasZappedByLoggedInUser.value) {
val zapsState by observeNoteZaps(baseNote)
val zapsState by observeNoteZaps(baseNote, accountViewModel)
LaunchedEffect(key1 = zapsState) {
if (zapsState?.note?.zapPayments?.isNotEmpty() == true || zapsState?.note?.zaps?.isNotEmpty() == true) {
@@ -1204,7 +1206,7 @@ fun ObserveZapAmountText(
accountViewModel: AccountViewModel,
inner: @Composable (String) -> Unit,
) {
val zapsState by observeNoteZaps(baseNote)
val zapsState by observeNoteZaps(baseNote, accountViewModel)
if (zapsState?.note?.zapPayments?.isNotEmpty() == true) {
@Suppress("ProduceStateDoesNotAssignValue")
@@ -69,6 +69,7 @@ fun ReplyInformationChannel(
ReplyInformationChannel(
replyTo,
sortedMentions,
accountViewModel = accountViewModel,
onUserTagClick = { nav.nav(routeFor(it)) },
)
Spacer(modifier = StdVertSpacer)
@@ -81,6 +82,7 @@ fun ReplyInformationChannel(
replyTo: ImmutableList<Note>?,
mentions: ImmutableList<User>?,
prefix: String = "",
accountViewModel: AccountViewModel,
onUserTagClick: (User) -> Unit,
) {
FlowRow {
@@ -93,7 +95,7 @@ fun ReplyInformationChannel(
)
mentions.forEachIndexed { idx, user ->
ReplyInfoMention(user, prefix, onUserTagClick)
ReplyInfoMention(user, prefix, accountViewModel, onUserTagClick)
if (idx < mentions.size - 2) {
Text(
@@ -118,9 +120,10 @@ fun ReplyInformationChannel(
private fun ReplyInfoMention(
user: User,
prefix: String,
accountViewModel: AccountViewModel,
onUserTagClick: (User) -> Unit,
) {
val innerUserState by observeUserInfo(user)
val innerUserState by observeUserInfo(user, accountViewModel)
CreateClickableTextWithEmoji(
clickablePart = "$prefix${innerUserState?.bestName()}",
@@ -385,7 +385,7 @@ private fun EmojiSelector(
accountViewModel,
) { emptyNote ->
emptyNote?.let { usersEmojiList ->
val collections by observeNoteEventAndMap(usersEmojiList) { event: EmojiPackSelectionEvent ->
val collections by observeNoteEventAndMap(usersEmojiList, accountViewModel) { event: EmojiPackSelectionEvent ->
event.emojiPackIds().toImmutableList()
}
@@ -57,7 +57,7 @@ fun UserCompose(
UsernameDisplay(baseUser, accountViewModel = accountViewModel)
}
AboutDisplay(baseUser)
AboutDisplay(baseUser, accountViewModel)
}
Column(modifier = remember { Modifier.padding(start = 10.dp) }) {
@@ -52,10 +52,10 @@ import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
@Composable
fun NoteAuthorPicture(
baseNote: Note,
nav: INav,
accountViewModel: AccountViewModel,
size: Dp,
pictureModifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: INav,
) {
NoteAuthorPicture(baseNote, size, accountViewModel, pictureModifier) {
nav.nav(routeFor(it))
@@ -304,7 +304,7 @@ fun BaseUserPicture(
outerModifier: Modifier = Modifier.size(size),
) {
Box(outerModifier, contentAlignment = Alignment.TopEnd) {
LoadUserProfilePicture(baseUser) { userProfilePicture, userName ->
LoadUserProfilePicture(baseUser, accountViewModel) { userProfilePicture, userName ->
InnerUserPicture(
userHex = baseUser.pubkeyHex,
userPicture = userProfilePicture,
@@ -326,9 +326,10 @@ fun BaseUserPicture(
@Composable
fun LoadUserProfilePicture(
baseUser: User,
accountViewModel: AccountViewModel,
innerContent: @Composable (String?, String?) -> Unit,
) {
val userProfile by observeUserInfo(baseUser)
val userProfile by observeUserInfo(baseUser, accountViewModel)
innerContent(userProfile?.profilePicture(), userProfile?.bestName())
}
@@ -54,7 +54,7 @@ fun NoteUsernameDisplay(
textColor: Color = Color.Unspecified,
accountViewModel: AccountViewModel,
) {
WatchAuthor(baseNote) {
WatchAuthor(baseNote, accountViewModel) {
UsernameDisplay(it, weight, textColor = textColor, accountViewModel = accountViewModel)
}
}
@@ -62,13 +62,14 @@ fun NoteUsernameDisplay(
@Composable
fun WatchAuthor(
baseNote: Note,
accountViewModel: AccountViewModel,
inner: @Composable (User) -> Unit,
) {
val noteAuthor = baseNote.author
if (noteAuthor != null) {
inner(noteAuthor)
} else {
val authorState by observeNote(baseNote)
val authorState by observeNote(baseNote, accountViewModel)
authorState?.note?.author?.let {
inner(it)
}
@@ -86,7 +87,7 @@ fun WatchAuthorWithBlank(
if (noteAuthor != null) {
inner(noteAuthor)
} else {
val authorState by observeNote(baseNote)
val authorState by observeNote(baseNote, accountViewModel)
CrossfadeIfEnabled(targetState = authorState?.note?.author, modifier = modifier, label = "WatchAuthorWithBlank", accountViewModel = accountViewModel) { newAuthor ->
inner(newAuthor)
}
@@ -101,7 +102,7 @@ fun UsernameDisplay(
textColor: Color = Color.Unspecified,
accountViewModel: AccountViewModel,
) {
val userMetadata by observeUserInfo(baseUser)
val userMetadata by observeUserInfo(baseUser, accountViewModel)
CrossfadeIfEnabled(targetState = userMetadata, modifier = weight, label = "UsernameDisplay", accountViewModel = accountViewModel) {
val name = it?.bestName()
@@ -74,7 +74,7 @@ fun WatchNoteEvent(
onNoteEventFound()
} else {
// avoid observing costs if already has an event.
val hasEvent by observeNoteHasEvent(baseNote)
val hasEvent by observeNoteHasEvent(baseNote, accountViewModel)
CrossfadeIfEnabled(targetState = hasEvent, label = "Event presence", accountViewModel = accountViewModel) {
if (it) {
onNoteEventFound()
@@ -66,7 +66,7 @@ fun ZapNoteCompose(
accountViewModel: AccountViewModel,
nav: INav,
) {
val baseNoteRequest by observeNote(baseReqResponse.zapRequest)
val baseNoteRequest by observeNote(baseReqResponse.zapRequest, accountViewModel)
var baseAuthor by remember { mutableStateOf<User?>(null) }
@@ -115,14 +115,14 @@ private fun RenderZapNote(
modifier = remember { Modifier.padding(start = 10.dp).weight(1f) },
) {
Row(verticalAlignment = Alignment.CenterVertically) { UsernameDisplay(baseAuthor, accountViewModel = accountViewModel) }
Row(verticalAlignment = Alignment.CenterVertically) { AboutDisplay(baseAuthor) }
Row(verticalAlignment = Alignment.CenterVertically) { AboutDisplay(baseAuthor, accountViewModel) }
}
Column(
modifier = remember { Modifier.padding(start = 10.dp) },
verticalArrangement = Arrangement.Center,
) {
ZapAmount(zapNote)
ZapAmount(zapNote, accountViewModel)
}
Column(modifier = Modifier.padding(start = 10.dp)) {
@@ -132,8 +132,11 @@ private fun RenderZapNote(
}
@Composable
private fun ZapAmount(zapEventNote: Note) {
val noteState by observeNote(zapEventNote)
private fun ZapAmount(
zapEventNote: Note,
accountViewModel: AccountViewModel,
) {
val noteState by observeNote(zapEventNote, accountViewModel)
var zapAmount by remember { mutableStateOf<String?>(null) }
@@ -174,7 +177,7 @@ fun ShowFollowingOrUnfollowingButton(
baseAuthor: User,
accountViewModel: AccountViewModel,
) {
var isFollowing = observeUserIsFollowing(accountViewModel.account.userProfile(), baseAuthor)
var isFollowing = observeUserIsFollowing(accountViewModel.account.userProfile(), baseAuthor, accountViewModel)
if (isFollowing.value) {
UnfollowButton {
@@ -202,8 +205,11 @@ fun ShowFollowingOrUnfollowingButton(
}
@Composable
fun AboutDisplay(baseAuthor: User) {
val aboutMe by observeUserAboutMe(baseAuthor)
fun AboutDisplay(
baseAuthor: User,
accountViewModel: AccountViewModel,
) {
val aboutMe by observeUserAboutMe(baseAuthor, accountViewModel)
Text(
aboutMe,
@@ -106,7 +106,7 @@ fun ZapUserSetCompose(
Column(modifier = remember { Modifier.padding(start = 10.dp).weight(1f) }) {
Row(verticalAlignment = Alignment.CenterVertically) { UsernameDisplay(zapSetCard.user, accountViewModel = accountViewModel) }
AboutDisplay(zapSetCard.user)
AboutDisplay(zapSetCard.user, accountViewModel)
}
}
@@ -37,14 +37,14 @@ fun WatchAndLoadMyEmojiList(accountViewModel: AccountViewModel) {
accountViewModel,
) { emptyNote ->
emptyNote?.let { usersEmojiList ->
val collections by observeNoteEventAndMap(usersEmojiList) { event: EmojiPackSelectionEvent ->
val collections by observeNoteEventAndMap(usersEmojiList, accountViewModel) { event: EmojiPackSelectionEvent ->
event.taggedAddresses().toImmutableList()
}
collections?.forEach { address ->
LoadAddressableNote(address, accountViewModel) { note ->
if (note != null) {
EventFinderFilterAssemblerSubscription(note)
EventFinderFilterAssemblerSubscription(note, accountViewModel)
}
}
}
@@ -55,7 +55,7 @@ fun ShowUserSuggestionList(
accountViewModel: AccountViewModel,
modifier: Modifier = Modifier.heightIn(0.dp, 200.dp),
) {
UserSearchDataSourceSubscription(userSuggestions)
UserSearchDataSourceSubscription(userSuggestions, accountViewModel)
val listState = rememberLazyListState()
@@ -135,7 +135,7 @@ fun UserLine(
)
}
AboutDisplay(baseUser)
AboutDisplay(baseUser, accountViewModel)
}
}
}
@@ -106,7 +106,7 @@ fun ObserveRelayListForDMs(
accountViewModel,
) { relayList ->
if (relayList != null) {
val relayListEvent by observeNoteEvent<ChatMessageRelayListEvent>(relayList)
val relayListEvent by observeNoteEvent<ChatMessageRelayListEvent>(relayList, accountViewModel)
inner(relayListEvent)
}
@@ -48,9 +48,9 @@ fun DefaultImageHeader(
note: Note,
accountViewModel: AccountViewModel,
) {
WatchAuthor(baseNote = note) {
WatchAuthor(baseNote = note, accountViewModel) {
Box {
BannerImage(it, Modifier.fillMaxWidth().heightIn(max = 200.dp))
BannerImage(it, Modifier.fillMaxWidth().heightIn(max = 200.dp), accountViewModel)
Box(authorNotePictureForImageHeader.align(Alignment.BottomStart)) {
BaseUserPicture(it, Size55dp, accountViewModel, Modifier)
@@ -63,8 +63,9 @@ fun DefaultImageHeader(
fun BannerImage(
author: User,
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
) {
val banner by observeUserBanner(author)
val banner by observeUserBanner(author, accountViewModel)
BannerImage(banner, modifier)
}
@@ -115,7 +115,7 @@ private fun RenderPledgeAmount(
baseReward: Reward,
accountViewModel: AccountViewModel,
) {
val repliesState by observeNoteReplies(baseNote)
val repliesState by observeNoteReplies(baseNote, accountViewModel)
var reward by remember {
mutableStateOf<String>(
showAmount(baseReward.amount),
@@ -382,8 +382,8 @@ fun WatchBookmarksFollowsAndAccount(
accountViewModel: AccountViewModel,
onNew: (DropDownParams) -> Unit,
) {
val followState by observeUserFollows(accountViewModel.userProfile())
val bookmarkState by observeUserBookmarks(accountViewModel.userProfile())
val followState by observeUserFollows(accountViewModel.userProfile(), accountViewModel)
val bookmarkState by observeUserBookmarks(accountViewModel.userProfile(), accountViewModel)
val showSensitiveContent by accountViewModel.showSensitiveContent().collectAsStateWithLifecycle()
LaunchedEffect(key1 = followState, key2 = bookmarkState, key3 = showSensitiveContent) {
@@ -82,7 +82,7 @@ fun ForkInformationRowLightColor(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteState by observeNote(originalVersion)
val noteState by observeNote(originalVersion, accountViewModel)
val note = noteState?.note ?: return
val author = note.author ?: return
val route = remember(note) { routeFor(note, accountViewModel.userProfile()) }
@@ -105,7 +105,7 @@ fun ForkInformationRowLightColor(
overflow = TextOverflow.Visible,
)
val userState by observeUser(author)
val userState by observeUser(author, accountViewModel)
userState?.user?.toBestDisplayName()?.let {
CreateClickableTextWithEmoji(
clickablePart = it,
@@ -128,19 +128,19 @@ fun ForkInformationRow(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteState by observeNote(originalVersion)
val noteState by observeNote(originalVersion, accountViewModel)
val note = noteState?.note ?: return
val route = remember(note) { routeFor(note, accountViewModel.userProfile()) }
if (route != null) {
Row(modifier) {
val author = note.author ?: return
val meta by observeUserInfo(author)
val meta by observeUserInfo(author, accountViewModel)
Text(stringRes(id = R.string.forked_from))
Spacer(modifier = StdHorzSpacer)
val userMetadata by observeUserInfo(author)
val userMetadata by observeUserInfo(author, accountViewModel)
CreateClickableTextWithEmoji(
clickablePart = remember(meta) { meta?.bestName() ?: author.pubkeyDisplayHex() },
@@ -188,7 +188,7 @@ fun ZapTheDevsCard(
accountViewModel: AccountViewModel,
nav: INav,
) {
val releaseNoteState by observeNote(baseNote)
val releaseNoteState by observeNote(baseNote, accountViewModel)
val releaseNote = releaseNoteState?.note ?: return
Row(modifier = Modifier.padding(start = Size10dp, end = Size10dp, bottom = Size10dp)) {
@@ -57,8 +57,11 @@ import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent
import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent
@Composable
fun BadgeDisplay(baseNote: Note) {
val badgeData by observeNoteEvent<BadgeDefinitionEvent>(baseNote)
fun BadgeDisplay(
baseNote: Note,
accountViewModel: AccountViewModel,
) {
val badgeData by observeNoteEvent<BadgeDefinitionEvent>(baseNote, accountViewModel)
badgeData?.let {
RenderBadge(
@@ -174,6 +177,6 @@ fun RenderBadgeAward(
}
note.replyTo?.firstOrNull()?.let {
BadgeDisplay(baseNote = it)
BadgeDisplay(baseNote = it, accountViewModel)
}
}
@@ -119,7 +119,7 @@ fun LongCommunityHeader(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent by observeNoteEvent<CommunityDefinitionEvent>(baseNote)
val noteEvent by observeNoteEvent<CommunityDefinitionEvent>(baseNote, accountViewModel)
Row(
lineModifier,
@@ -195,7 +195,7 @@ fun LongCommunityHeader(
modifier = Modifier.width(75.dp),
)
Spacer(DoubleHorzSpacer)
NoteAuthorPicture(baseNote, nav, accountViewModel, Size25dp)
NoteAuthorPicture(baseNote, Size25dp, accountViewModel = accountViewModel, nav = nav)
Spacer(DoubleHorzSpacer)
NoteUsernameDisplay(baseNote, Modifier.weight(1f), accountViewModel = accountViewModel)
}
@@ -264,7 +264,7 @@ fun ShortCommunityHeader(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent by observeNoteEvent<CommunityDefinitionEvent>(baseNote)
val noteEvent by observeNoteEvent<CommunityDefinitionEvent>(baseNote, accountViewModel)
Row(verticalAlignment = Alignment.CenterVertically) {
noteEvent?.image()?.let {
@@ -70,7 +70,7 @@ public fun RenderEmojiPack(
accountViewModel: AccountViewModel,
onClick: ((EmojiUrlTag) -> Unit)? = null,
) {
val noteEvent by observeNoteEvent<EmojiPackEvent>(baseNote)
val noteEvent by observeNoteEvent<EmojiPackEvent>(baseNote, accountViewModel)
noteEvent?.let {
RenderEmojiPack(
@@ -179,7 +179,9 @@ private fun EmojiListOptions(
accountViewModel,
) {
it?.let { usersEmojiList ->
val hasAddedThis by observeNoteAndMap(usersEmojiList) { usersEmojiList.event?.isTaggedAddressableNote(emojiPackNote.idHex) }
val hasAddedThis by observeNoteAndMap(usersEmojiList, accountViewModel) {
usersEmojiList.event?.isTaggedAddressableNote(emojiPackNote.idHex)
}
CrossfadeIfEnabled(targetState = hasAddedThis, label = "EmojiListOptions", accountViewModel = accountViewModel) {
if (it != true) {
@@ -67,7 +67,7 @@ private fun ObserverAndRenderNIP95(
) {
val eventHeader = (header.event as? FileStorageHeaderEvent) ?: return
val noteState by observeNote(content)
val noteState by observeNote(content, accountViewModel)
val content by
remember(noteState) {
@@ -100,7 +100,7 @@ private fun RenderShortRepositoryHeader(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent by observeNoteEvent<GitRepositoryEvent>(baseNote)
val noteEvent by observeNoteEvent<GitRepositoryEvent>(baseNote, accountViewModel)
Column(
modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp),
@@ -236,7 +236,7 @@ fun DisplayEntryForUser(
accountViewModel: AccountViewModel,
nav: INav,
) {
val userMetadata by observeUserInfo(baseUser)
val userMetadata by observeUserInfo(baseUser, accountViewModel)
CreateClickableTextWithEmoji(
clickablePart = userMetadata?.bestName() ?: baseUser.pubkeyDisplayHex(),
@@ -254,12 +254,12 @@ fun DisplayEntryForNote(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteState by observeNote(note)
val noteState by observeNote(note, accountViewModel)
val author = userBase ?: noteState?.note?.author
if (author != null) {
RenderUserAsClickableText(author, null, nav)
RenderUserAsClickableText(author, null, accountViewModel, nav)
}
val noteEvent = noteState?.note?.event as? BaseThreadedEvent ?: return
@@ -65,19 +65,19 @@ fun RenderInteractiveStory(
val address = baseNote.address() ?: return
// keep updating the root event with new versions
val note = observeNote(baseNote)
val note = observeNote(baseNote, accountViewModel)
val rootEvent = note.value?.note?.event as? InteractiveStoryBaseEvent ?: return
// keep updating the reading state event with new versions
val readingStateNote = accountViewModel.getInteractiveStoryReadingState(address.toValue())
val readingState by observeNoteEvent<InteractiveStoryReadingStateEvent>(readingStateNote)
val readingState by observeNoteEvent<InteractiveStoryReadingStateEvent>(readingStateNote, accountViewModel)
val currentScene = readingState?.currentScene()
if (currentScene != null && currentScene != rootEvent.address()) {
LoadAddressableNote(currentScene, accountViewModel) { currentSceneBaseNote ->
currentSceneBaseNote?.let {
val currentSceneEvent by observeNoteEvent<InteractiveStoryBaseEvent>(it)
val currentSceneEvent by observeNoteEvent<InteractiveStoryBaseEvent>(it, accountViewModel)
currentSceneEvent?.let {
RenderInteractiveStory(
@@ -166,7 +166,7 @@ fun RenderInteractiveStory(
options.forEach { opt ->
LoadAddressableNote(opt.address, accountViewModel) { note ->
if (note != null) {
EventFinderFilterAssemblerSubscription(note)
EventFinderFilterAssemblerSubscription(note, accountViewModel)
OutlinedButton(
onClick = { onSelect(note) },
@@ -130,7 +130,7 @@ fun RenderLiveActivityEventInner(
) {
val noteEvent = baseNote.event as? LiveActivitiesEvent ?: return
val eventUpdates by observeNote(baseNote)
val eventUpdates by observeNote(baseNote, accountViewModel)
val media = remember(eventUpdates) { noteEvent.streaming() }
val cover = remember(eventUpdates) { noteEvent.image() }
@@ -222,7 +222,7 @@ fun RenderLiveActivityEventInner(
) {
AsyncImage(model = it, contentDescription = null, modifier = MaterialTheme.colorScheme.imageModifier)
}
} ?: run { DisplayAuthorBanner(baseNote) }
} ?: run { DisplayAuthorBanner(baseNote, accountViewModel) }
Text(
text = stringRes(id = R.string.live_stream_has_ended),
@@ -278,7 +278,7 @@ private fun RelayOptionsAction(
accountViewModel: AccountViewModel,
nav: INav,
) {
val isCurrentlyOnTheUsersList by observeUserRelayIntoList(accountViewModel.userProfile(), relay)
val isCurrentlyOnTheUsersList by observeUserRelayIntoList(accountViewModel.userProfile(), relay, accountViewModel)
if (isCurrentlyOnTheUsersList) {
AddRelayButton {
@@ -131,7 +131,7 @@ fun RenderTextModificationEvent(
noteEvent.editedNote()?.let {
LoadNote(baseNoteHex = it.eventId, accountViewModel = accountViewModel) { baseNote ->
baseNote?.let {
val noteState by observeNote(baseNote)
val noteState by observeNote(baseNote, accountViewModel)
val editStateOriginalNote =
observeEdits(baseNote = baseNote, accountViewModel = accountViewModel)
@@ -223,7 +223,7 @@ fun ShortTorrentHeader(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent by observeNoteEvent<TorrentEvent>(baseNote)
val noteEvent by observeNoteEvent<TorrentEvent>(baseNote, accountViewModel)
ShortTorrentHeader(
title = noteEvent?.title() ?: TorrentEvent.ALT_DESCRIPTION,
@@ -28,11 +28,12 @@ import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.screen.FollowListState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.ChatroomListKnownFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.ChatroomListNewFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.dal.DiscoverChatFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.dal.DiscoverCommunityFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.dal.DiscoverLiveFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.dal.DiscoverMarketplaceFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.dal.DiscoverNIP89FeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.DiscoverChatFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.DiscoverFollowSetsFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.DiscoverLiveFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.DiscoverCommunityFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.DiscoverNIP89FeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.DiscoverMarketplaceFeedFilter
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.HomeNewThreadFeedFilter
@@ -53,6 +54,7 @@ class AccountFeedContentStates(
val videoFeed = FeedContentState(VideoFeedFilter(accountViewModel.account), accountViewModel.viewModelScope)
val discoverFollowSets = FeedContentState(DiscoverFollowSetsFeedFilter(accountViewModel.account), accountViewModel.viewModelScope)
val discoverMarketplace = FeedContentState(DiscoverMarketplaceFeedFilter(accountViewModel.account), accountViewModel.viewModelScope)
val discoverDVMs = FeedContentState(DiscoverNIP89FeedFilter(accountViewModel.account), accountViewModel.viewModelScope)
val discoverLive = FeedContentState(DiscoverLiveFeedFilter(accountViewModel.account), accountViewModel.viewModelScope)
@@ -151,6 +151,7 @@ import okhttp3.OkHttpClient
class AccountViewModel(
accountSettings: AccountSettings,
val settings: SharedSettingsState,
val app: Amethyst,
) : ViewModel(),
Dao {
val account = Account(accountSettings, accountSettings.createSigner(), viewModelScope)
@@ -1010,7 +1011,7 @@ class AccountViewModel(
.verifyNip05(
nip05,
okttpClient = {
Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP05(it))
app.okHttpClients.getHttpClient(account.shouldUseTorForNIP05(it))
},
onSuccess = {
// Marks user as verified
@@ -1196,16 +1197,16 @@ class AccountViewModel(
onReady: (ImmutableList<User>) -> Unit,
) {
viewModelScope.launch(Dispatchers.Default) {
onReady(
hexList
.mapNotNull { hex -> checkGetOrCreateUser(hex) }
.sortedBy { account.isFollowing(it) }
.reversed()
.toImmutableList(),
)
onReady(loadUsersSync(hexList).toImmutableList())
}
}
fun loadUsersSync(hexList: List<String>): List<User> =
hexList
.mapNotNull { hex -> checkGetOrCreateUser(hex) }
.sortedBy { account.isFollowing(it) }
.reversed()
fun loadUsers(
event: GeneralListEvent,
onReady: (ImmutableList<User>) -> Unit,
@@ -1292,43 +1293,44 @@ class AccountViewModel(
viewModelScope.launch(Dispatchers.IO) {
// Only restart relay connections if port or type changes
if (account.settings.setTorSettings(newTorSettings)) {
Amethyst.instance.serviceManager.forceRestart()
app.serviceManager.forceRestart()
}
}
fun forceRestartServices() =
viewModelScope.launch(Dispatchers.IO) {
Amethyst.instance.serviceManager.setAccountAndRestart(account)
app.serviceManager.setAccountAndRestart(account)
}
fun justStart() =
viewModelScope.launch(Dispatchers.IO) {
Amethyst.instance.serviceManager.justStartIfItHasAccount()
app.serviceManager.justStartIfItHasAccount()
}
fun justPause() =
viewModelScope.launch(Dispatchers.IO) {
Amethyst.instance.serviceManager.cleanObservers()
Amethyst.instance.serviceManager.pauseForGood()
app.serviceManager.cleanObservers()
app.serviceManager.pauseForGood()
}
fun pauseAndLogOff() =
viewModelScope.launch(Dispatchers.IO) {
Amethyst.instance.serviceManager.cleanObservers()
Amethyst.instance.serviceManager.pauseAndLogOff()
app.serviceManager.cleanObservers()
app.serviceManager.pauseAndLogOff()
}
fun changeProxyPort(port: Int) =
viewModelScope.launch(Dispatchers.IO) {
Amethyst.instance.serviceManager.forceRestart()
app.serviceManager.forceRestart()
}
class Factory(
val accountSettings: AccountSettings,
val settings: SharedSettingsState,
val app: Amethyst,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = AccountViewModel(accountSettings, settings) as T
override fun <T : ViewModel> create(modelClass: Class<T>): T = AccountViewModel(accountSettings, settings, app) as T
}
private var collectorJob: Job? = null
@@ -1537,23 +1539,23 @@ class AccountViewModel(
}
}
fun proxyPortFor(url: String): Int? = Amethyst.instance.okHttpClients.getCurrentProxyPort(account.shouldUseTorForVideoDownload(url))
fun proxyPortFor(url: String): Int? = app.okHttpClients.getCurrentProxyPort(account.shouldUseTorForVideoDownload(url))
fun okHttpClientForNip96(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(url))
fun okHttpClientForNip96(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(url))
fun okHttpClientForImage(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForImageDownload(url))
fun okHttpClientForImage(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForImageDownload(url))
fun okHttpClientForVideo(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForVideoDownload(url))
fun okHttpClientForVideo(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForVideoDownload(url))
fun okHttpClientForMoney(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForMoneyOperations(url))
fun okHttpClientForMoney(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForMoneyOperations(url))
fun okHttpClientForPreview(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForPreviewUrl(url))
fun okHttpClientForPreview(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForPreviewUrl(url))
fun okHttpClientForDirty(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForDirty(url))
fun okHttpClientForDirty(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForDirty(url))
fun okHttpClientForTrustedRelays(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForTrustedRelays())
fun okHttpClientForTrustedRelays(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForTrustedRelays())
fun dataSources() = Amethyst.instance.sources
fun dataSources() = app.sources
suspend fun deleteDraft(draftTag: String) {
account.deleteDraft(draftTag)
@@ -1699,7 +1701,7 @@ class AccountViewModel(
suspend fun findUsersStartingWithSync(prefix: String) = LocalCache.findUsersStartingWith(prefix, account)
fun relayStatusFlow() = Amethyst.instance.client.relayStatusFlow()
fun relayStatusFlow() = app.client.relayStatusFlow()
fun allAccountsSync(): List<HexKey> =
runBlocking {
@@ -1802,6 +1804,7 @@ fun mockAccountViewModel(): AccountViewModel {
),
),
sharedPreferencesViewModel.sharedPrefs,
Amethyst(),
)
}
@@ -1819,5 +1822,6 @@ fun mockVitorAccountViewModel(): AccountViewModel {
),
),
sharedPreferencesViewModel.sharedPrefs,
Amethyst(),
)
}
@@ -76,6 +76,7 @@ fun LoggedInPage(
AccountViewModel.Factory(
accountSettings,
sharedPreferencesViewModel.sharedPrefs,
Amethyst.instance,
),
)
@@ -97,7 +98,7 @@ fun LoggedInPage(
DiscoveryFilterAssemblerSubscription(accountViewModel)
// TODO: Is this needed?
RelaySubscriptionsCoordinatorSubscription()
RelaySubscriptionsCoordinatorSubscription(accountViewModel)
// Updates local cache of the anti-spam filter choice of this user.
ObserveAntiSpamFilterSettings(accountViewModel)
@@ -329,7 +329,7 @@ fun NewPostScreen(
}
Row {
Notifying(postViewModel.pTags?.toImmutableList()) {
Notifying(postViewModel.pTags?.toImmutableList(), accountViewModel) {
postViewModel.removeFromReplyList(it)
}
}
@@ -579,6 +579,7 @@ fun DisplayPreviews(
@Composable
fun Notifying(
baseMentions: ImmutableList<User>?,
accountViewModel: AccountViewModel,
onClick: (User) -> Unit,
) {
val mentions = baseMentions?.toSet()
@@ -601,7 +602,7 @@ fun Notifying(
),
onClick = { onClick(user) },
) {
DisplayUserNameWithDeleteMark(user)
DisplayUserNameWithDeleteMark(user, accountViewModel)
}
}
}
@@ -609,8 +610,11 @@ fun Notifying(
}
@Composable
private fun DisplayUserNameWithDeleteMark(user: User) {
val innerUserState by observeUser(user)
private fun DisplayUserNameWithDeleteMark(
user: User,
accountViewModel: AccountViewModel,
) {
val innerUserState by observeUser(user, accountViewModel)
innerUserState?.user?.let { myUser ->
CreateTextWithEmoji(
text = remember(innerUserState) { "${myUser.toBestDisplayName()}" },
@@ -55,7 +55,7 @@ private fun WatchAndDisplayUser(
accountViewModel: AccountViewModel,
nav: INav,
) {
val userState by observeUserInfo(author)
val userState by observeUserInfo(author, accountViewModel)
UserDisplayNameLayout(
picture = {
@@ -47,7 +47,7 @@ fun RoomNameOnlyDisplay(
fontWeight: FontWeight = FontWeight.Bold,
accountViewModel: AccountViewModel,
) {
val roomSubject by observeUserRoomSubject(accountViewModel.userProfile(), room)
val roomSubject by observeUserRoomSubject(accountViewModel.userProfile(), room, accountViewModel)
CrossfadeIfEnabled(targetState = roomSubject, modifier, accountViewModel = accountViewModel) {
if (!it.isNullOrBlank()) {
@@ -98,7 +98,7 @@ fun RoomNameDisplay(
modifier: Modifier,
accountViewModel: AccountViewModel,
) {
val roomSubject by observeUserRoomSubject(accountViewModel.userProfile(), room)
val roomSubject by observeUserRoomSubject(accountViewModel.userProfile(), room, accountViewModel)
CrossfadeIfEnabled(targetState = roomSubject, modifier, accountViewModel = accountViewModel) {
if (!it.isNullOrBlank()) {
@@ -158,7 +158,7 @@ fun ShortUsernameDisplay(
fontWeight: FontWeight = FontWeight.Bold,
accountViewModel: AccountViewModel,
) {
val userName by observeUserShortName(baseUser)
val userName by observeUserShortName(baseUser, accountViewModel)
CrossfadeIfEnabled(targetState = userName, modifier = weight, accountViewModel = accountViewModel) {
CreateTextWithEmoji(
@@ -82,7 +82,7 @@ fun ShortEphemeralChatChannelHeader(
accountViewModel: AccountViewModel,
nav: INav,
) {
val channelState by observeChannel(baseChannel)
val channelState by observeChannel(baseChannel, accountViewModel)
val channel = channelState?.channel as? EphemeralChatChannel ?: return
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -153,7 +153,7 @@ fun JoinEphemeralChatButtonIfNotAlreadyJoined(
accountViewModel: AccountViewModel,
nav: INav,
) {
val isFollowing by observeUserIsFollowingChannel(accountViewModel.account, channel)
val isFollowing by observeUserIsFollowingChannel(accountViewModel.account, channel, accountViewModel)
if (!isFollowing) {
JoinChatButton(channel, accountViewModel, nav)
@@ -73,7 +73,7 @@ fun LongPublicChatChannelHeader(
accountViewModel: AccountViewModel,
nav: INav,
) {
val channelState by observeChannel(baseChannel)
val channelState by observeChannel(baseChannel, accountViewModel)
val channel = channelState?.channel as? PublicChatChannel ?: return
Spacer(StdVertSpacer)
@@ -147,7 +147,7 @@ fun LongPublicChatChannelHeader(
modifier = Modifier.width(75.dp),
)
Spacer(DoubleHorzSpacer)
NoteAuthorPicture(note, nav, accountViewModel, Size25dp)
NoteAuthorPicture(note, Size25dp, accountViewModel = accountViewModel, nav = nav)
Spacer(DoubleHorzSpacer)
NoteUsernameDisplay(note, Modifier.weight(1f), accountViewModel = accountViewModel)
}
@@ -211,7 +211,7 @@ fun LeaveButtonIfFollowing(
accountViewModel: AccountViewModel,
nav: INav,
) {
val isFollowing by observeUserIsFollowingChannel(accountViewModel.account, channel)
val isFollowing by observeUserIsFollowingChannel(accountViewModel.account, channel, accountViewModel)
if (isFollowing) {
LeaveChatButton(channel, accountViewModel, nav)
@@ -60,7 +60,7 @@ fun ShortPublicChatChannelHeader(
accountViewModel: AccountViewModel,
nav: INav,
) {
val channelState by observeChannel(baseChannel)
val channelState by observeChannel(baseChannel, accountViewModel)
val channel = channelState?.channel as? PublicChatChannel ?: return
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -139,7 +139,7 @@ fun JoinChatButtonIfNotAlreadyJoined(
accountViewModel: AccountViewModel,
nav: INav,
) {
val isFollowing by observeUserIsFollowingChannel(accountViewModel.account, channel)
val isFollowing by observeUserIsFollowingChannel(accountViewModel.account, channel, accountViewModel)
if (!isFollowing) {
JoinChatButton(channel, accountViewModel, nav)
@@ -77,7 +77,7 @@ fun LongLiveActivityChannelHeader(
accountViewModel: AccountViewModel,
nav: INav,
) {
val channelState by observeChannel(baseChannel)
val channelState by observeChannel(baseChannel, accountViewModel)
val channel = channelState?.channel as? LiveActivitiesChannel ?: return
Row(
@@ -129,7 +129,7 @@ fun LongLiveActivityChannelHeader(
modifier = Modifier.width(75.dp),
)
Spacer(DoubleHorzSpacer)
NoteAuthorPicture(note, nav, accountViewModel, Size25dp)
NoteAuthorPicture(note, Size25dp, accountViewModel = accountViewModel, nav = nav)
Spacer(DoubleHorzSpacer)
NoteUsernameDisplay(note, Modifier.weight(1f), accountViewModel = accountViewModel)
}
@@ -48,7 +48,7 @@ fun ShortLiveActivityChannelHeader(
nav: INav,
showFlag: Boolean,
) {
val channelState by observeChannel(baseChannel)
val channelState by observeChannel(baseChannel, accountViewModel)
val channel = channelState?.channel as? LiveActivitiesChannel ?: return
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -42,7 +42,7 @@ fun ShowVideoStreaming(
accountViewModel: AccountViewModel,
) {
baseChannel.info?.let {
val streamingInfoEvent by observeChannelInfo(baseChannel)
val streamingInfoEvent by observeChannelInfo(baseChannel, accountViewModel)
streamingInfoEvent?.let { event ->
event.streaming()?.let { url ->
val zoomableUrlVideo =
@@ -105,7 +105,7 @@ fun ChatroomHeaderCompose(
if (baseNote.event != null) {
ChatroomComposeChannelOrUser(baseNote, accountViewModel, nav)
} else {
val hasEvent by observeNoteHasEvent(baseNote)
val hasEvent by observeNoteHasEvent(baseNote, accountViewModel)
if (hasEvent) {
ChatroomComposeChannelOrUser(baseNote, accountViewModel, nav)
} else {
@@ -186,8 +186,8 @@ private fun ChannelRoomCompose(
accountViewModel: AccountViewModel,
nav: INav,
) {
val authorName by observeUserName(note.author!!)
val channelState by observeChannel(channel)
val authorName by observeUserName(note.author!!, accountViewModel)
val channelState by observeChannel(channel, accountViewModel)
val channelPicture = channelState?.channel?.profilePicture() ?: channel.profilePicture()
val channelName = channelState?.channel?.toBestDisplayName() ?: channel.toBestDisplayName()
@@ -227,8 +227,8 @@ private fun ChannelRoomCompose(
accountViewModel: AccountViewModel,
nav: INav,
) {
val authorName by observeUserName(note.author!!)
val channelState by observeChannel(channel)
val authorName by observeUserName(note.author!!, accountViewModel)
val channelState by observeChannel(channel, accountViewModel)
val relayInfo = loadRelayInfo(channel.roomId.relayUrl, accountViewModel)
val info =
@@ -0,0 +1,219 @@
/**
* Copyright (c) 2024 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.discover
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport
import com.vitorpamplona.amethyst.ui.note.ClickableNote
import com.vitorpamplona.amethyst.ui.note.LongPressToQuickAction
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
import com.vitorpamplona.amethyst.ui.note.calculateBackgroundColor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.RenderChannelThumb
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.RenderFollowSetThumb
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.RenderLiveActivityThumb
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.RenderCommunitiesThumb
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.RenderContentDVMThumb
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.RenderClassifiedsThumb
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import com.vitorpamplona.amethyst.ui.theme.StdPadding
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
@Composable
fun ChannelCardCompose(
baseNote: Note,
routeForLastRead: String? = null,
modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null,
forceEventKind: Int?,
isHiddenFeed: Boolean = false,
accountViewModel: AccountViewModel,
nav: INav,
) {
WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) {
if (forceEventKind == null || baseNote.event?.kind == forceEventKind) {
CheckHiddenFeedWatchBlockAndReport(
note = baseNote,
modifier = modifier,
ignoreAllBlocksAndReports = isHiddenFeed,
showHiddenWarning = false,
accountViewModel = accountViewModel,
nav = nav,
) { canPreview ->
NormalChannelCard(
baseNote = baseNote,
routeForLastRead = routeForLastRead,
modifier = modifier,
parentBackgroundColor = parentBackgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
}
}
@Composable
fun NormalChannelCard(
baseNote: Note,
routeForLastRead: String? = null,
modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
nav: INav,
) {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
CheckNewAndRenderChannelCard(
baseNote,
routeForLastRead,
modifier,
parentBackgroundColor,
accountViewModel,
showPopup,
nav,
)
}
}
@Composable
private fun CheckNewAndRenderChannelCard(
baseNote: Note,
routeForLastRead: String? = null,
modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
showPopup: () -> Unit,
nav: INav,
) {
val backgroundColor =
calculateBackgroundColor(
createdAt = baseNote.createdAt(),
routeForLastRead = routeForLastRead,
parentBackgroundColor = parentBackgroundColor,
accountViewModel = accountViewModel,
)
ClickableNote(
baseNote = baseNote,
backgroundColor = backgroundColor,
modifier = modifier,
accountViewModel = accountViewModel,
showPopup = showPopup,
nav = nav,
) {
InnerChannelCardWithReactions(
baseNote = baseNote,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
@Composable
fun InnerChannelCardWithReactions(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
when (baseNote.event) {
is LiveActivitiesEvent -> InnerCardRow(baseNote, accountViewModel, nav)
is CommunityDefinitionEvent -> InnerCardRow(baseNote, accountViewModel, nav)
is ChannelCreateEvent -> InnerCardRow(baseNote, accountViewModel, nav)
is ClassifiedsEvent -> InnerCardBox(baseNote, accountViewModel, nav)
is AppDefinitionEvent -> InnerCardRow(baseNote, accountViewModel, nav)
is FollowListEvent -> InnerCardRow(baseNote, accountViewModel, nav)
}
}
@Composable
fun InnerCardRow(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
Column(StdPadding) {
SensitivityWarning(
note = baseNote,
accountViewModel = accountViewModel,
) {
RenderNoteRow(
baseNote,
accountViewModel,
nav,
)
}
}
}
@Composable
fun InnerCardBox(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
Column(HalfPadding) {
SensitivityWarning(
note = baseNote,
accountViewModel = accountViewModel,
) {
RenderClassifiedsThumb(baseNote, accountViewModel, nav)
}
}
}
@Composable
private fun RenderNoteRow(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
when (baseNote.event) {
is LiveActivitiesEvent -> {
RenderLiveActivityThumb(baseNote, accountViewModel, nav)
}
is CommunityDefinitionEvent -> {
RenderCommunitiesThumb(baseNote, accountViewModel, nav)
}
is ChannelCreateEvent -> {
RenderChannelThumb(baseNote, accountViewModel, nav)
}
is AppDefinitionEvent -> {
RenderContentDVMThumb(baseNote, accountViewModel, nav)
}
is FollowListEvent -> {
RenderFollowSetThumb(baseNote, accountViewModel, nav)
}
}
}
@@ -52,7 +52,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
@@ -73,7 +72,6 @@ import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.note.ChannelCardCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.TabItem
@@ -84,6 +82,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size26Modifier
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
@@ -98,6 +97,7 @@ fun DiscoverScreen(
nav: INav,
) {
DiscoverScreen(
discoveryFollowSetsFeedContentState = accountViewModel.feedStates.discoverFollowSets,
discoveryContentNIP89FeedContentState = accountViewModel.feedStates.discoverDVMs,
discoveryMarketplaceFeedContentState = accountViewModel.feedStates.discoverMarketplace,
discoveryLiveFeedContentState = accountViewModel.feedStates.discoverLive,
@@ -111,6 +111,7 @@ fun DiscoverScreen(
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DiscoverScreen(
discoveryFollowSetsFeedContentState: FeedContentState,
discoveryContentNIP89FeedContentState: FeedContentState,
discoveryMarketplaceFeedContentState: FeedContentState,
discoveryLiveFeedContentState: FeedContentState,
@@ -119,18 +120,17 @@ fun DiscoverScreen(
accountViewModel: AccountViewModel,
nav: INav,
) {
val lifeCycleOwner = LocalLifecycleOwner.current
val tabs by
remember(
discoveryContentNIP89FeedContentState,
discoveryLiveFeedContentState,
discoveryCommunityFeedContentState,
discoveryChatFeedContentState,
discoveryMarketplaceFeedContentState,
) {
remember(accountViewModel) {
mutableStateOf(
listOf(
TabItem(
R.string.discover_follows,
discoveryFollowSetsFeedContentState,
"DiscoverFollowSets",
ScrollStateKeys.DISCOVER_CONTENT,
FollowListEvent.KIND,
),
TabItem(
R.string.discover_content,
discoveryContentNIP89FeedContentState,
@@ -174,6 +174,7 @@ fun DiscoverScreen(
val pagerState = rememberForeverPagerState(key = PagerStateKeys.DISCOVER_SCREEN) { tabs.size }
WatchAccountForDiscoveryScreen(
discoveryFollowSetsFeedContentState = discoveryFollowSetsFeedContentState,
discoveryContentNIP89FeedContentState = discoveryContentNIP89FeedContentState,
discoveryMarketplaceFeedContentState = discoveryMarketplaceFeedContentState,
discoveryLiveFeedContentState = discoveryLiveFeedContentState,
@@ -182,6 +183,7 @@ fun DiscoverScreen(
accountViewModel = accountViewModel,
)
WatchLifecycleAndUpdateModel(discoveryFollowSetsFeedContentState)
WatchLifecycleAndUpdateModel(discoveryContentNIP89FeedContentState)
WatchLifecycleAndUpdateModel(discoveryMarketplaceFeedContentState)
WatchLifecycleAndUpdateModel(discoveryLiveFeedContentState)
@@ -376,6 +378,7 @@ private fun RenderDiscoverFeed(
@Composable
fun WatchAccountForDiscoveryScreen(
discoveryFollowSetsFeedContentState: FeedContentState,
discoveryContentNIP89FeedContentState: FeedContentState,
discoveryMarketplaceFeedContentState: FeedContentState,
discoveryLiveFeedContentState: FeedContentState,
@@ -386,6 +389,7 @@ fun WatchAccountForDiscoveryScreen(
val listState by accountViewModel.account.liveDiscoveryFollowLists.collectAsStateWithLifecycle()
LaunchedEffect(accountViewModel, listState) {
discoveryFollowSetsFeedContentState.checkKeysInvalidateDataAndSendToTop()
discoveryContentNIP89FeedContentState.checkKeysInvalidateDataAndSendToTop()
discoveryMarketplaceFeedContentState.checkKeysInvalidateDataAndSendToTop()
discoveryLiveFeedContentState.checkKeysInvalidateDataAndSendToTop()
@@ -40,6 +40,7 @@ fun DiscoveryTopBar(
FollowListWithoutRoutes(
followListsModel = accountViewModel.feedStates.feedListOptions,
listName = list,
accountViewModel,
) { listName ->
accountViewModel.account.settings.changeDefaultDiscoveryFollowList(listName.code)
}
@@ -33,6 +33,7 @@ import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
@@ -238,6 +239,25 @@ class DiscoveryFilterAssembler(
)
}
fun createFollowSetFilter(key: DiscoveryQueryState): List<TypedFilter> {
val follows =
key.account.liveDiscoveryListAuthorsPerRelay.value
?.ifEmpty { null }
return listOfNotNull(
TypedFilter(
types = setOf(FeedType.FOLLOWS),
filter =
SinceAuthorPerRelayFilter(
authors = follows,
kinds = listOf(FollowListEvent.KIND),
limit = 500,
since = since(key),
),
),
)
}
fun createCommunitiesFilter(key: DiscoveryQueryState): TypedFilter {
val follows = key.account.liveDiscoveryListAuthorsPerRelay.value
@@ -450,6 +470,7 @@ class DiscoveryFilterAssembler(
createLiveStreamFilter(key)
.plus(createNIP89Filter(key))
.plus(createPublicChatFilter(key))
.plus(createFollowSetFilter(key))
.plus(createMarketplaceFilter(key))
.plus(
listOfNotNull(
@@ -18,7 +18,7 @@
* 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.discover.dal
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
@@ -38,9 +38,9 @@ open class DiscoverChatFeedFilter(
override fun showHiddenKey(): Boolean =
account.settings.defaultDiscoveryFollowList.value ==
PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
PeopleListEvent.Companion.blockListFor(account.userProfile().pubkeyHex) ||
account.settings.defaultDiscoveryFollowList.value ==
MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
MuteListEvent.Companion.blockListFor(account.userProfile().pubkeyHex)
override fun feed(): List<Note> {
val params = buildFilterParams(account)
@@ -67,7 +67,7 @@ open class DiscoverChatFeedFilter(
override fun applyFilter(collection: Set<Note>): Set<Note> = innerApplyFilter(collection)
fun buildFilterParams(account: Account): FilterByListParams =
FilterByListParams.create(
FilterByListParams.Companion.create(
userHex = account.userProfile().pubkeyHex,
selectedListName = account.settings.defaultDiscoveryFollowList.value,
followLists = account.liveDiscoveryFollowLists.value,
@@ -0,0 +1,198 @@
/**
* Copyright (c) 2024 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.discover.nip28Chats
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePainter
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner
import com.vitorpamplona.amethyst.ui.note.Gallery
import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.LoadChannel
import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.HalfTopPadding
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun RenderChannelThumb(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent = baseNote.event as? ChannelCreateEvent ?: return
LoadChannel(baseChannelHex = baseNote.idHex, accountViewModel) {
RenderChannelThumb(baseNote = baseNote, channel = it, accountViewModel, nav)
}
}
@Composable
fun RenderChannelThumb(
baseNote: Note,
channel: Channel,
accountViewModel: AccountViewModel,
nav: INav,
) {
val channelUpdates by observeChannel(channel, accountViewModel)
val name = remember(channelUpdates) { channelUpdates?.channel?.toBestDisplayName() ?: "" }
val description = remember(channelUpdates) { channelUpdates?.channel?.summary()?.ifBlank { null } }
var cover by
remember(channelUpdates) {
mutableStateOf(channelUpdates?.channel?.profilePicture()?.ifBlank { null })
}
var participantUsers by
remember(baseNote) {
mutableStateOf<ImmutableList<User>>(
persistentListOf(),
)
}
LaunchedEffect(key1 = channelUpdates) {
launch(Dispatchers.IO) {
val followingKeySet =
accountViewModel.account.liveDiscoveryFollowLists.value
?.authors
val allParticipants =
ParticipantListBuilder()
.followsThatParticipateOn(baseNote, followingKeySet)
.toImmutableList()
val newParticipantUsers =
if (followingKeySet == null) {
val allFollows = accountViewModel.account.liveKind3Follows.value.authors
val followingParticipants =
ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).toList()
(followingParticipants + (allParticipants - followingParticipants)).toImmutableList()
} else {
allParticipants.toImmutableList()
}
if (!equalImmutableLists(newParticipantUsers, participantUsers)) {
participantUsers = newParticipantUsers
}
}
}
LeftPictureLayout(
onImage = {
cover?.let {
AsyncImage(
model = it,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier =
Modifier
.fillMaxSize()
.clip(QuoteBorder),
onState = {
if (it is AsyncImagePainter.State.Error) {
cover = null
}
},
)
} ?: run { DisplayAuthorBanner(baseNote, accountViewModel) }
},
onTitleRow = {
Text(
text = name,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f),
)
Spacer(modifier = StdHorzSpacer)
Row(
verticalAlignment = CenterVertically,
horizontalArrangement = RowColSpacing,
) {
LikeReaction(
baseNote = baseNote,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav,
)
}
Spacer(modifier = StdHorzSpacer)
ZapReaction(
baseNote = baseNote,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav = nav,
)
},
onDescription = {
Text(
text = description ?: stringRes(R.string.chat_about_topic, name),
color = MaterialTheme.colorScheme.grayText,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
fontSize = 14.sp,
lineHeight = 18.sp,
modifier = HalfTopPadding,
)
},
onBottomRow = {
if (participantUsers.isNotEmpty()) {
Gallery(participantUsers, HalfTopPadding, accountViewModel, nav)
}
},
)
}
@@ -0,0 +1,76 @@
/**
* Copyright (c) 2024 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.discover.nip51FollowSets
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
open class DiscoverFollowSetsFeedFilter(
val account: Account,
) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList()
open fun followList(): String = account.settings.defaultDiscoveryFollowList.value
override fun showHiddenKey(): Boolean =
followList() == PeopleListEvent.Companion.blockListFor(account.userProfile().pubkeyHex) ||
followList() == MuteListEvent.Companion.blockListFor(account.userProfile().pubkeyHex)
override fun feed(): List<Note> {
val params = buildFilterParams(account)
val notes =
LocalCache.addressables.filterIntoSet { _, it ->
val noteEvent = it.event
noteEvent is FollowListEvent && params.match(noteEvent)
}
return sort(notes)
}
override fun applyFilter(collection: Set<Note>): Set<Note> = innerApplyFilter(collection)
fun buildFilterParams(account: Account): FilterByListParams =
FilterByListParams.Companion.create(
account.userProfile().pubkeyHex,
account.settings.defaultDiscoveryFollowList.value,
account.liveDiscoveryFollowLists.value,
account.flowHiddenUsers.value,
)
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val params = buildFilterParams(account)
return collection.filterTo(HashSet()) {
val noteEvent = it.event
noteEvent is FollowListEvent && params.match(noteEvent)
}
}
override fun sort(collection: Set<Note>): List<Note> = collection.sortedWith(DefaultFeedOrder)
}
@@ -0,0 +1,204 @@
/**
* Copyright (c) 2024 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.discover.nip51FollowSets
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner
import com.vitorpamplona.amethyst.ui.note.Gallery
import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@Immutable
data class FollowSetCard(
val name: String,
val media: String?,
val description: String?,
val users: ImmutableList<User>,
)
@Composable
fun RenderFollowSetThumb(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
val card by observeNoteAndMap(baseNote, accountViewModel) {
val noteEvent = it.event as? FollowListEvent
FollowSetCard(
name = noteEvent?.nameOrTitle()?.ifBlank { null } ?: noteEvent?.dTag() ?: "",
media = noteEvent?.image()?.ifBlank { null },
description = noteEvent?.description(),
users =
accountViewModel
.loadUsersSync(
noteEvent?.pubKeys() ?: emptyList(),
).toImmutableList(),
)
}
RenderFollowSetThumb(
card,
baseNote,
accountViewModel,
nav,
)
}
@Preview
@Composable
fun RenderFollowSetThumbPreview() {
val accountViewModel = mockAccountViewModel()
val nav = EmptyNav
ThemeComparisonColumn(
toPreview = {
RenderFollowSetThumb(
card =
FollowSetCard(
"Orange Pill Perú",
"https://i.postimg.cc/GtDgGY5v/5062563795762785335.jpg",
"Desc",
persistentListOf(
accountViewModel.userProfile(),
accountViewModel.userProfile(),
accountViewModel.userProfile(),
accountViewModel.userProfile(),
accountViewModel.userProfile(),
),
),
baseNote = Note(""),
accountViewModel = accountViewModel,
nav = nav,
)
},
)
}
@Composable
fun RenderFollowSetThumb(
card: FollowSetCard,
baseNote: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
Column(
modifier = Modifier.fillMaxWidth(),
) {
Box(
contentAlignment = Alignment.BottomStart,
) {
card.media?.let {
AsyncImage(
model = it,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier =
Modifier
.fillMaxWidth()
.aspectRatio(ratio = 21f / 9f)
.clip(QuoteBorder),
)
} ?: run { DisplayAuthorBanner(baseNote, accountViewModel) }
Gallery(card.users, Modifier.padding(Size10dp), accountViewModel, nav)
}
Spacer(modifier = DoubleVertSpacer)
Row(
verticalAlignment = CenterVertically,
horizontalArrangement = RowColSpacing5dp,
) {
Text(
text = card.name,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f),
)
Spacer(modifier = StdHorzSpacer)
LikeReaction(
baseNote = baseNote,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav,
)
Spacer(modifier = StdHorzSpacer)
ZapReaction(
baseNote = baseNote,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav = nav,
)
}
baseNote.author?.let { author ->
Spacer(modifier = DoubleVertSpacer)
Row(
verticalAlignment = CenterVertically,
horizontalArrangement = RowColSpacing5dp,
) {
UserPicture(author, Size25dp, accountViewModel = accountViewModel, nav = nav)
UsernameDisplay(author, fontWeight = FontWeight.Normal, accountViewModel = accountViewModel)
}
}
}
}
@@ -18,7 +18,7 @@
* 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.discover.dal
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
@@ -39,8 +39,8 @@ open class DiscoverLiveFeedFilter(
open fun followList(): String = account.settings.defaultDiscoveryFollowList.value
override fun showHiddenKey(): Boolean =
followList() == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
followList() == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
followList() == PeopleListEvent.Companion.blockListFor(account.userProfile().pubkeyHex) ||
followList() == MuteListEvent.Companion.blockListFor(account.userProfile().pubkeyHex)
override fun feed(): List<Note> {
val allChannelNotes = LocalCache.channels.mapNotNull { _, channel -> LocalCache.getNoteIfExists(channel.idHex) }
@@ -55,7 +55,7 @@ open class DiscoverLiveFeedFilter(
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val filterParams =
FilterByListParams.create(
FilterByListParams.Companion.create(
userHex = account.userProfile().pubkeyHex,
selectedListName = account.settings.defaultDiscoveryFollowList.value,
followLists = account.liveDiscoveryFollowLists.value,
@@ -0,0 +1,255 @@
/**
* Copyright (c) 2024 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.discover.nip53LiveActivities
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.BottomStart
import androidx.compose.ui.Alignment.Companion.TopEnd
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner
import com.vitorpamplona.amethyst.ui.note.Gallery
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.EndedFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.OfflineFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ScheduledFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Immutable
data class LiveActivityCard(
val name: String,
val cover: String?,
val media: String?,
val subject: String?,
val content: String?,
val participants: ImmutableList<ParticipantTag>,
val status: String?,
val starts: Long?,
)
@Composable
fun RenderLiveActivityThumb(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
val card by observeNoteAndMap(baseNote, accountViewModel) {
val noteEvent = it.event as? LiveActivitiesEvent
LiveActivityCard(
name = noteEvent?.dTag() ?: "",
cover = noteEvent?.image()?.ifBlank { null },
media = noteEvent?.streaming(),
subject = noteEvent?.title()?.ifBlank { null },
content = noteEvent?.summary(),
participants = noteEvent?.participants()?.toImmutableList() ?: persistentListOf(),
status = noteEvent?.status(),
starts = noteEvent?.starts(),
)
}
RenderLiveActivityThumb(
card,
baseNote,
accountViewModel,
nav,
)
}
@Composable
fun RenderLiveActivityThumb(
card: LiveActivityCard,
baseNote: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
Column(
modifier = Modifier.fillMaxWidth(),
) {
Box(
contentAlignment = TopEnd,
modifier =
Modifier
.aspectRatio(ratio = 16f / 9f)
.fillMaxWidth(),
) {
card.cover?.let {
AsyncImage(
model = it,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier =
Modifier
.fillMaxSize()
.clip(QuoteBorder),
)
} ?: run { DisplayAuthorBanner(baseNote, accountViewModel) }
Box(Modifier.padding(10.dp)) {
CrossfadeIfEnabled(targetState = card.status, label = "RenderLiveActivityThumb", accountViewModel = accountViewModel) {
when (it) {
StatusTag.STATUS.LIVE.code -> {
val url = card.media
if (url.isNullOrBlank()) {
LiveFlag()
} else {
CheckIfVideoIsOnline(url, accountViewModel) { isOnline ->
if (isOnline) {
LiveFlag()
} else {
OfflineFlag()
}
}
}
}
StatusTag.STATUS.ENDED.code -> {
EndedFlag()
}
StatusTag.STATUS.PLANNED.code -> {
ScheduledFlag(card.starts)
}
else -> {
EndedFlag()
}
}
}
}
LoadParticipants(card.participants, baseNote, accountViewModel) { participantUsers ->
Box(
Modifier
.padding(10.dp)
.align(BottomStart),
) {
if (participantUsers.isNotEmpty()) {
Gallery(participantUsers, Modifier, accountViewModel, nav)
}
}
}
}
Spacer(modifier = DoubleVertSpacer)
ChannelHeader(
channelHex = baseNote.idHex,
showVideo = false,
showFlag = false,
sendToChannel = true,
modifier = Modifier,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
@Composable
fun LoadParticipants(
participants: ImmutableList<ParticipantTag>,
baseNote: Note,
accountViewModel: AccountViewModel,
inner: @Composable (ImmutableList<User>) -> Unit,
) {
var participantUsers by remember {
mutableStateOf<ImmutableList<User>>(
persistentListOf(),
)
}
LaunchedEffect(key1 = participants) {
launch(Dispatchers.IO) {
val hosts =
participants.mapNotNull { part ->
if (part.pubKey != baseNote.author?.pubkeyHex) {
LocalCache.checkGetOrCreateUser(part.pubKey)
} else {
null
}
}
val hostsAuthor = hosts + (baseNote.author?.let { listOf(it) } ?: emptyList<User>())
val followingKeySet =
accountViewModel.account.liveDiscoveryFollowLists.value
?.authors
val allParticipants =
ParticipantListBuilder()
.followsThatParticipateOn(baseNote, followingKeySet)
.minus(hostsAuthor)
val newParticipantUsers =
if (followingKeySet == null) {
val allFollows = accountViewModel.account.liveKind3Follows.value.authors
val followingParticipants =
ParticipantListBuilder()
.followsThatParticipateOn(baseNote, allFollows)
.minus(hostsAuthor)
(hosts + followingParticipants + (allParticipants - followingParticipants))
.toImmutableList()
} else {
(hosts + allParticipants).toImmutableList()
}
if (!equalImmutableLists(newParticipantUsers, participantUsers)) {
participantUsers = newParticipantUsers
}
}
}
inner(participantUsers)
}
@@ -0,0 +1,225 @@
/**
* Copyright (c) 2024 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.discover.nip72Communities
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.BottomStart
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner
import com.vitorpamplona.amethyst.ui.note.Gallery
import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.HalfTopPadding
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Immutable
data class CommunityCard(
val name: String,
val description: String?,
val cover: String?,
val moderators: ImmutableList<HexKey>,
)
@Composable
fun RenderCommunitiesThumb(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteState by observeNote(baseNote, accountViewModel)
val noteEvent = noteState.note.event as? CommunityDefinitionEvent ?: return
RenderCommunitiesThumb(
CommunityCard(
name = noteEvent.dTag(),
description = noteEvent.description(),
cover = noteEvent.image()?.imageUrl,
moderators = noteEvent.moderatorKeys().toImmutableList(),
),
baseNote,
accountViewModel,
nav,
)
}
@Composable
fun RenderCommunitiesThumb(
card: CommunityCard,
baseNote: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
LeftPictureLayout(
onImage = {
card.cover?.let {
Box(contentAlignment = BottomStart) {
AsyncImage(
model = it,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier =
Modifier
.fillMaxSize()
.clip(QuoteBorder),
)
}
} ?: run { DisplayAuthorBanner(baseNote, accountViewModel) }
},
onTitleRow = {
Text(
text = card.name,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f),
)
Spacer(modifier = StdHorzSpacer)
Row(
verticalAlignment = CenterVertically,
horizontalArrangement = RowColSpacing,
) {
LikeReaction(
baseNote = baseNote,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav,
)
}
Spacer(modifier = StdHorzSpacer)
ZapReaction(
baseNote = baseNote,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav = nav,
)
},
onDescription = {
Text(
text = card.description ?: stringRes(R.string.community_about_topic, card.name),
color = MaterialTheme.colorScheme.grayText,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
fontSize = 14.sp,
lineHeight = 18.sp,
modifier = HalfTopPadding,
)
},
onBottomRow = {
LoadModerators(card.moderators, baseNote, accountViewModel) { participantUsers ->
if (participantUsers.isNotEmpty()) {
Gallery(participantUsers, HalfTopPadding, accountViewModel, nav)
}
}
},
)
}
@Composable
fun LoadModerators(
moderators: ImmutableList<String>,
baseNote: Note,
accountViewModel: AccountViewModel,
content: @Composable (ImmutableList<User>) -> Unit,
) {
var participantUsers by remember {
mutableStateOf<ImmutableList<User>>(
persistentListOf(),
)
}
LaunchedEffect(key1 = moderators) {
launch(Dispatchers.IO) {
val hosts =
moderators.mapNotNull { part ->
if (part != baseNote.author?.pubkeyHex) {
LocalCache.checkGetOrCreateUser(part)
} else {
null
}
}
val followingKeySet =
accountViewModel.account.liveDiscoveryFollowLists.value
?.authors
val allParticipants =
ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hosts)
val newParticipantUsers =
if (followingKeySet == null) {
val allFollows = accountViewModel.account.liveKind3Follows.value.authors
val followingParticipants =
ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hosts)
(hosts + followingParticipants + (allParticipants - followingParticipants))
.toImmutableList()
} else {
(hosts + allParticipants).toImmutableList()
}
if (!equalImmutableLists(newParticipantUsers, participantUsers)) {
participantUsers = newParticipantUsers
}
}
}
content(participantUsers)
}
@@ -18,7 +18,7 @@
* 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.discover.dal
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
@@ -38,13 +38,13 @@ open class DiscoverCommunityFeedFilter(
override fun showHiddenKey(): Boolean =
account.settings.defaultDiscoveryFollowList.value ==
PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
PeopleListEvent.Companion.blockListFor(account.userProfile().pubkeyHex) ||
account.settings.defaultDiscoveryFollowList.value ==
MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
MuteListEvent.Companion.blockListFor(account.userProfile().pubkeyHex)
override fun feed(): List<Note> {
val filterParams =
FilterByListParams.create(
FilterByListParams.Companion.create(
userHex = account.userProfile().pubkeyHex,
selectedListName = account.settings.defaultDiscoveryFollowList.value,
followLists = account.liveDiscoveryFollowLists.value,
@@ -55,7 +55,7 @@ open class DiscoverCommunityFeedFilter(
val notes =
LocalCache.addressables.mapNotNullIntoSet { key, note ->
val noteEvent = note.event
if (noteEvent == null && shouldInclude(Address.parse(key), filterParams)) {
if (noteEvent == null && shouldInclude(Address.Companion.parse(key), filterParams)) {
// send unloaded communities to the screen
note
} else if (noteEvent is CommunityDefinitionEvent && filterParams.match(noteEvent)) {
@@ -73,7 +73,7 @@ open class DiscoverCommunityFeedFilter(
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
// here, we need to look for CommunityDefinition in new collection AND new CommunityDefinition from Post Approvals
val filterParams =
FilterByListParams.create(
FilterByListParams.Companion.create(
userHex = account.userProfile().pubkeyHex,
selectedListName = account.settings.defaultDiscoveryFollowList.value,
followLists = account.liveDiscoveryFollowLists.value,
@@ -109,7 +109,7 @@ open class DiscoverCommunityFeedFilter(
private fun shouldInclude(
aTag: Address?,
params: FilterByListParams,
) = aTag != null && aTag.kind == CommunityDefinitionEvent.KIND && params.match(aTag)
) = aTag != null && aTag.kind == CommunityDefinitionEvent.Companion.KIND && params.match(aTag)
override fun sort(collection: Set<Note>): List<Note> {
val lastNote =
@@ -0,0 +1,219 @@
/**
* Copyright (c) 2024 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.discover.nip90DVMs
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment.Companion.BottomStart
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.observeAppDefinition
import com.vitorpamplona.amethyst.ui.theme.HalfTopPadding
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.bitcoinColor
import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.amethyst.ui.theme.nip05
@Immutable
data class DVMCard(
val name: String,
val description: String?,
val cover: String?,
val amount: String?,
val personalized: Boolean?,
)
@Composable
fun RenderContentDVMThumb(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
// downloads user metadata to pre-load the NIP-65 relays.
baseNote.author?.let { UserFinderFilterAssemblerSubscription(it, accountViewModel) }
val card = observeAppDefinition(appDefinitionNote = baseNote, accountViewModel)
LeftPictureLayout(
imageFraction = 0.20f,
onImage = {
card.cover?.let {
Box(contentAlignment = BottomStart) {
AsyncImage(
model = it,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier =
Modifier
.fillMaxSize()
.clip(QuoteBorder),
)
}
} ?: run {
baseNote.author?.let {
BannerImage(
it,
Modifier
.fillMaxSize()
.clip(QuoteBorder),
accountViewModel,
)
}
}
},
onTitleRow = {
Text(
text = card.name,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f),
)
Spacer(modifier = StdVertSpacer)
Row(
verticalAlignment = CenterVertically,
horizontalArrangement = RowColSpacing5dp,
) {
LikeReaction(
baseNote = baseNote,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav,
)
}
Spacer(modifier = StdHorzSpacer)
ZapReaction(
baseNote = baseNote,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav = nav,
)
},
onDescription = {
card.description?.let {
Text(
text = it,
color = MaterialTheme.colorScheme.grayText,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
fontSize = 14.sp,
lineHeight = 16.sp,
modifier = HalfTopPadding,
)
}
},
onBottomRow = {
card.amount?.let {
var color = Color.DarkGray
var amount = it
if (card.amount == "free" || card.amount == "0") {
color = MaterialTheme.colorScheme.secondary
amount = "Free"
} else if (card.amount == "flexible") {
color = MaterialTheme.colorScheme.primaryContainer
amount = "Flexible"
} else if (card.amount == "") {
color = MaterialTheme.colorScheme.grayText
amount = "Unknown"
} else {
color = MaterialTheme.colorScheme.primary
amount = card.amount + " Sats"
}
Row(
verticalAlignment = CenterVertically,
horizontalArrangement = Arrangement.Absolute.Right,
) {
Text(
textAlign = TextAlign.End,
text = " $amount ",
color = color,
maxLines = 3,
modifier =
Modifier
.weight(1f, fill = false)
.border(Dp(.1f), color, shape = RoundedCornerShape(20)),
fontSize = 12.sp,
)
}
}
Spacer(modifier = StdHorzSpacer)
card.personalized?.let {
var color = Color.DarkGray
var name = "generic"
if (card.personalized == true) {
color = MaterialTheme.colorScheme.bitcoinColor
name = "Personalized"
} else {
color = MaterialTheme.colorScheme.nip05
name = "Generic"
}
Spacer(modifier = StdVertSpacer)
Row(
verticalAlignment = CenterVertically,
horizontalArrangement = Arrangement.Absolute.Right,
) {
Text(
textAlign = TextAlign.End,
text = " $name ",
color = color,
maxLines = 3,
modifier =
Modifier
.padding(start = 4.dp)
.weight(1f, fill = false)
.border(Dp(.1f), color, shape = RoundedCornerShape(20)),
fontSize = 12.sp,
)
}
}
},
)
}
@@ -18,7 +18,7 @@
* 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.discover.dal
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
@@ -41,8 +41,8 @@ open class DiscoverNIP89FeedFilter(
open fun followList(): String = account.settings.defaultDiscoveryFollowList.value
override fun showHiddenKey(): Boolean =
followList() == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
followList() == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
followList() == PeopleListEvent.Companion.blockListFor(account.userProfile().pubkeyHex) ||
followList() == MuteListEvent.Companion.blockListFor(account.userProfile().pubkeyHex)
override fun feed(): List<Note> {
val notes =
@@ -56,7 +56,7 @@ open class DiscoverNIP89FeedFilter(
override fun applyFilter(collection: Set<Note>): Set<Note> = innerApplyFilter(collection)
fun buildFilterParams(account: Account): FilterByListParams =
FilterByListParams.create(
FilterByListParams.Companion.create(
account.userProfile().pubkeyHex,
account.settings.defaultDiscoveryFollowList.value,
account.liveDiscoveryFollowLists.value,
@@ -0,0 +1,164 @@
/**
* Copyright (c) 2024 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.discover.nip99Classifieds
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.BottomStart
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner
import com.vitorpamplona.amethyst.ui.note.showAmountInteger
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
import com.vitorpamplona.quartz.nip99Classifieds.tags.PriceTag
@Immutable
data class ClassifiedsThumb(
val image: String?,
val title: String?,
val price: PriceTag?,
)
@Composable
fun RenderClassifiedsThumb(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
if (baseNote.event !is ClassifiedsEvent) return
val card by observeNoteAndMap(baseNote, accountViewModel) {
val noteEvent = it.event as? ClassifiedsEvent
ClassifiedsThumb(
image = noteEvent?.image(),
title = noteEvent?.title(),
price = noteEvent?.price(),
)
}
InnerRenderClassifiedsThumb(card, baseNote, accountViewModel)
}
@Preview
@Composable
fun RenderClassifiedsThumbPreview() {
Surface(Modifier.size(200.dp)) {
InnerRenderClassifiedsThumb(
card =
ClassifiedsThumb(
image = null,
title = "Like New",
price = PriceTag("800000", "SATS", null),
),
note = Note("hex"),
mockAccountViewModel(),
)
}
}
@Composable
fun InnerRenderClassifiedsThumb(
card: ClassifiedsThumb,
note: Note,
accountViewModel: AccountViewModel,
) {
Box(
Modifier
.fillMaxWidth()
.aspectRatio(1f),
contentAlignment = BottomStart,
) {
card.image?.let {
AsyncImage(
model = it,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)
} ?: run { DisplayAuthorBanner(note, accountViewModel) }
Row(
Modifier
.fillMaxWidth()
.background(Color.Black.copy(0.6f))
.padding(Size5dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
card.title?.let {
Text(
text = it,
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = Color.White,
modifier = Modifier.weight(1f),
)
}
card.price?.let {
val priceTag =
remember(card) {
val newAmount = it.amount.toBigDecimalOrNull()?.let { showAmountInteger(it) } ?: it.amount
if (it.frequency != null && it.currency != null) {
"$newAmount ${it.currency}/${it.frequency}"
} else if (it.currency != null) {
"$newAmount ${it.currency}"
} else {
newAmount
}
}
Text(
text = priceTag,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = Color.White,
)
}
}
}
}
@@ -18,7 +18,7 @@
* 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.discover.dal
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
@@ -37,8 +37,8 @@ open class DiscoverMarketplaceFeedFilter(
open fun followList(): String = account.settings.defaultDiscoveryFollowList.value
override fun showHiddenKey(): Boolean =
followList() == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
followList() == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
followList() == PeopleListEvent.Companion.blockListFor(account.userProfile().pubkeyHex) ||
followList() == MuteListEvent.Companion.blockListFor(account.userProfile().pubkeyHex)
override fun feed(): List<Note> {
val params = buildFilterParams(account)
@@ -55,7 +55,7 @@ open class DiscoverMarketplaceFeedFilter(
override fun applyFilter(collection: Set<Note>): Set<Note> = innerApplyFilter(collection)
fun buildFilterParams(account: Account): FilterByListParams =
FilterByListParams.create(
FilterByListParams.Companion.create(
account.userProfile().pubkeyHex,
account.settings.defaultDiscoveryFollowList.value,
account.liveDiscoveryFollowLists.value,
@@ -18,7 +18,7 @@
* 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.discover.marketplace
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds
import android.net.Uri
import androidx.compose.foundation.horizontalScroll
@@ -18,7 +18,7 @@
* 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.discover.marketplace
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds
import android.content.Context
import android.util.Log
@@ -18,7 +18,7 @@
* 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.discover.marketplace
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.Box
@@ -70,7 +70,6 @@ import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.DVMCard
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon
import com.vitorpamplona.amethyst.ui.note.PayViaIntentDialog
@@ -83,6 +82,7 @@ import com.vitorpamplona.amethyst.ui.note.payViaIntent
import com.vitorpamplona.amethyst.ui.screen.RenderFeedState
import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.DVMCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.dal.NIP90ContentDiscoveryFeedViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
@@ -195,7 +195,7 @@ fun ObserverContentDiscoveryResponse(
) {
val noteAuthor = appDefinition.author ?: return
EventFinderFilterAssemblerSubscription(dvmRequestId)
EventFinderFilterAssemblerSubscription(dvmRequestId, accountViewModel)
val resultFlow =
remember(dvmRequestId) {
@@ -321,7 +321,7 @@ fun FeedDVM(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
val card = observeAppDefinition(appDefinitionNote)
val card = observeAppDefinition(appDefinitionNote, accountViewModel)
card.cover?.let {
AsyncImage(
@@ -333,7 +333,7 @@ fun FeedDVM(
.size(Size75dp)
.clip(QuoteBorder),
)
} ?: run { NoteAuthorPicture(appDefinitionNote, nav, accountViewModel, Size75dp) }
} ?: run { NoteAuthorPicture(appDefinitionNote, Size75dp, accountViewModel = accountViewModel, nav = nav) }
Spacer(modifier = DoubleVertSpacer)
@@ -449,7 +449,7 @@ fun ZapDVMButton(
}
// Makes sure the user is loaded to get his ln address ahead of time.
UserFinderFilterAssemblerSubscription(noteAuthor)
UserFinderFilterAssemblerSubscription(noteAuthor, accountViewModel)
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -589,7 +589,7 @@ fun FeedEmptyWithStatus(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
val card = observeAppDefinition(appDefinitionNote)
val card = observeAppDefinition(appDefinitionNote, accountViewModel)
card.cover?.let {
AsyncImage(
@@ -601,7 +601,7 @@ fun FeedEmptyWithStatus(
.size(Size75dp)
.clip(QuoteBorder),
)
} ?: run { NoteAuthorPicture(appDefinitionNote, nav, accountViewModel, Size75dp) }
} ?: run { NoteAuthorPicture(appDefinitionNote, Size75dp, accountViewModel = accountViewModel, nav = nav) }
Spacer(modifier = DoubleVertSpacer)
@@ -641,8 +641,11 @@ fun convertAppMetadataToCard(metadata: AppMetadata?): DVMCard {
}
@Composable
fun observeAppDefinition(appDefinitionNote: Note): DVMCard {
val card by observeNoteAndMap(appDefinitionNote) {
fun observeAppDefinition(
appDefinitionNote: Note,
accountViewModel: AccountViewModel,
): DVMCard {
val card by observeNoteAndMap(appDefinitionNote, accountViewModel) {
convertAppMetadataToCard((it.event as? AppDefinitionEvent)?.appMetaData())
}
return card
@@ -49,7 +49,7 @@ fun DvmTopBar(
title = {
LoadNote(baseNoteHex = appDefinitionId, accountViewModel = accountViewModel) { appDefinitionNote ->
if (appDefinitionNote != null) {
val card = observeAppDefinition(appDefinitionNote)
val card = observeAppDefinition(appDefinitionNote, accountViewModel)
card.cover?.let {
AsyncImage(
@@ -121,7 +121,7 @@ fun GeoHashActionOptions(
tag: String,
accountViewModel: AccountViewModel,
) {
val isFollowingTag by observeUserIsFollowingGeohash(accountViewModel.userProfile(), tag)
val isFollowingTag by observeUserIsFollowingGeohash(accountViewModel.userProfile(), tag, accountViewModel)
if (isFollowingTag) {
UnfollowButton {
@@ -138,7 +138,7 @@ fun HashtagActionOptions(
tag: String,
accountViewModel: AccountViewModel,
) {
val isFollowingTag by observeUserIsFollowingHashtag(accountViewModel.userProfile(), tag)
val isFollowingTag by observeUserIsFollowingHashtag(accountViewModel.userProfile(), tag, accountViewModel)
if (isFollowingTag) {
UnfollowButton {
@@ -40,6 +40,7 @@ fun HomeTopBar(
FollowListWithRoutes(
followListsModel = accountViewModel.feedStates.feedListOptions,
listName = list,
accountViewModel = accountViewModel,
) { listName ->
if (listName.route != null) {
nav.nav(listName.route)
@@ -20,18 +20,17 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.live
import android.R.attr.onClick
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.model.EphemeralChatChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelNoteAuthors
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.nav
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.note.Gallery
@@ -50,7 +49,7 @@ fun RenderEphemeralBubble(
nav.nav { routeFor(channel) }
},
) {
RenderUsers(channel, accountViewModel)
RenderUsers(channel, accountViewModel, nav)
Spacer(StdHorzSpacer)
Text(
channel.toBestDisplayName(),
@@ -62,8 +61,9 @@ fun RenderEphemeralBubble(
fun RenderUsers(
channel: EphemeralChatChannel,
accountViewModel: AccountViewModel,
nav: INav,
) {
val authors by observeChannelNoteAuthors(channel)
val authors by observeChannelNoteAuthors(channel, accountViewModel)
Gallery(authors, Modifier, accountViewModel, 3)
Gallery(authors, Modifier, accountViewModel, nav, 3)
}
@@ -40,6 +40,7 @@ fun NotificationTopBar(
FollowListWithoutRoutes(
followListsModel = accountViewModel.feedStates.feedListOptions,
listName = list,
accountViewModel,
) { listName ->
accountViewModel.account.settings.changeDefaultNotificationFollowList(listName.code)
}
@@ -402,6 +402,7 @@ private fun RenderScreen(
CreateAndRenderTabs(
baseUser,
pagerState,
accountViewModel,
)
}
HorizontalPager(
@@ -484,6 +485,7 @@ fun UpdateThreadsAndRepliesWhenBlockUnblock(
private fun CreateAndRenderTabs(
baseUser: User,
pagerState: PagerState,
accountViewModel: AccountViewModel,
) {
val coroutineScope = rememberCoroutineScope()
@@ -493,13 +495,13 @@ private fun CreateAndRenderTabs(
{ Text(text = stringRes(R.string.replies)) },
{ Text(text = stringRes(R.string.mutual)) },
{ Text(text = stringRes(R.string.gallery)) },
{ FollowTabHeader(baseUser) },
{ FollowersTabHeader(baseUser) },
{ ZapTabHeader(baseUser) },
{ BookmarkTabHeader(baseUser) },
{ FollowedTagsTabHeader(baseUser) },
{ ReportsTabHeader(baseUser) },
{ RelaysTabHeader(baseUser) },
{ FollowTabHeader(baseUser, accountViewModel) },
{ FollowersTabHeader(baseUser, accountViewModel) },
{ ZapTabHeader(baseUser, accountViewModel) },
{ BookmarkTabHeader(baseUser, accountViewModel) },
{ FollowedTagsTabHeader(baseUser, accountViewModel) },
{ ReportsTabHeader(baseUser, accountViewModel) },
{ RelaysTabHeader(baseUser, accountViewModel) },
)
tabs.forEachIndexed { index, function ->
@@ -26,11 +26,15 @@ import androidx.compose.runtime.getValue
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserBookmarkCount
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@Composable
fun BookmarkTabHeader(baseUser: User) {
val userBookmarks by observeUserBookmarkCount(baseUser)
fun BookmarkTabHeader(
baseUser: User,
accountViewModel: AccountViewModel,
) {
val userBookmarks by observeUserBookmarkCount(baseUser, accountViewModel)
Text(text = "$userBookmarks ${stringRes(R.string.bookmarks)}")
}
@@ -26,11 +26,15 @@ import androidx.compose.runtime.getValue
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollowerCount
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@Composable
fun FollowersTabHeader(baseUser: User) {
val followerCount by observeUserFollowerCount(baseUser)
fun FollowersTabHeader(
baseUser: User,
accountViewModel: AccountViewModel,
) {
val followerCount by observeUserFollowerCount(baseUser, accountViewModel)
val text =
if (followerCount > 0) {

Some files were not shown because too many files have changed in this diff Show More