Merge branch 'main' into amber

This commit is contained in:
greenart7c3
2023-08-21 08:50:56 -03:00
committed by GitHub
66 changed files with 1211 additions and 175 deletions
@@ -299,7 +299,8 @@ object LocalPreferences {
val followingCommunities = getStringSet(PrefKeys.FOLLOWING_COMMUNITIES, null) ?: setOf()
val hiddenUsers = getStringSet(PrefKeys.HIDDEN_USERS, emptySet()) ?: setOf()
val localRelays = getString(PrefKeys.RELAYS, "[]")?.let {
Event.mapper.readValue<Set<RelaySetupInfo>>(it)
println("LocalRelays: $it")
Event.mapper.readValue<Set<RelaySetupInfo>?>(it)
} ?: setOf<RelaySetupInfo>()
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
@@ -310,24 +311,24 @@ object LocalPreferences {
val defaultDiscoveryFollowList = getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
val zapAmountChoices = getString(PrefKeys.ZAP_AMOUNTS, "[]")?.let {
Event.mapper.readValue<List<Long>>(it)
Event.mapper.readValue<List<Long>?>(it)
}?.ifEmpty { listOf(500L, 1000L, 5000L) } ?: listOf(500L, 1000L, 5000L)
val reactionChoices = getString(PrefKeys.REACTION_CHOICES, "[]")?.let {
Event.mapper.readValue<List<String>>(it)
Event.mapper.readValue<List<String>?>(it)
}?.ifEmpty { listOf("+") } ?: listOf("+")
val defaultZapType = getString(PrefKeys.DEFAULT_ZAPTYPE, "")?.let { serverName ->
LnZapEvent.ZapType.values().firstOrNull { it.name == serverName }
LnZapEvent.ZapType.values().firstOrNull() { it.name == serverName }
} ?: LnZapEvent.ZapType.PUBLIC
val defaultFileServer = getString(PrefKeys.DEFAULT_FILE_SERVER, "")?.let { serverName ->
ServersAvailable.values().firstOrNull { it.name == serverName }
ServersAvailable.values().firstOrNull() { it.name == serverName }
} ?: ServersAvailable.NOSTR_BUILD
val zapPaymentRequestServer = try {
getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let {
Event.mapper.readValue<Nip47URI>(it)
Event.mapper.readValue<Nip47URI?>(it)
}
} catch (e: Throwable) {
e.printStackTrace()
@@ -345,7 +346,7 @@ object LocalPreferences {
val languagePreferences = try {
getString(PrefKeys.LANGUAGE_PREFS, null)?.let {
Event.mapper.readValue<Map<String, String>>(it)
Event.mapper.readValue<Map<String, String>?>(it)
} ?: mapOf()
} catch (e: Throwable) {
e.printStackTrace()
@@ -369,7 +370,7 @@ object LocalPreferences {
val lastReadPerRoute = try {
getString(PrefKeys.LAST_READ_PER_ROUTE, null)?.let {
Event.mapper.readValue<Map<String, Long>>(it)
Event.mapper.readValue<Map<String, Long>?>(it)
} ?: mapOf()
} catch (e: Throwable) {
e.printStackTrace()
@@ -26,15 +26,35 @@ class PlaybackService : MediaSessionService() {
return ProgressiveMediaSource.Factory(VideoCache.get(Amethyst.instance, HttpClient.getHttpClient()))
}
fun lazyHlsDS(): MultiPlayerPlaybackManager {
managerHls?.let { return it }
val newInstance = MultiPlayerPlaybackManager(newHslDataSource(), videoViewedPositionCache)
managerHls = newInstance
return newInstance
}
fun lazyProgressiveDS(): MultiPlayerPlaybackManager {
managerProgressive?.let { return it }
val newInstance = MultiPlayerPlaybackManager(newProgressiveDataSource(), videoViewedPositionCache)
managerProgressive = newInstance
return newInstance
}
fun lazyLocalDS(): MultiPlayerPlaybackManager {
managerLocal?.let { return it }
val newInstance = MultiPlayerPlaybackManager(cachedPositions = videoViewedPositionCache)
managerLocal = newInstance
return newInstance
}
// Create your Player and MediaSession in the onCreate lifecycle event
@OptIn(UnstableApi::class)
override fun onCreate() {
super.onCreate()
managerHls = MultiPlayerPlaybackManager(newHslDataSource(), videoViewedPositionCache)
managerProgressive = MultiPlayerPlaybackManager(newProgressiveDataSource(), videoViewedPositionCache)
managerLocal = MultiPlayerPlaybackManager(cachedPositions = videoViewedPositionCache)
// Stop all videos and recreates all managers when the proxy changes.
HttpClient.proxyChangeListeners.add(this@PlaybackService::onProxyUpdated)
}
@@ -62,11 +82,11 @@ class PlaybackService : MediaSessionService() {
fun getAppropriateMediaSessionManager(fileName: String): MultiPlayerPlaybackManager? {
return if (fileName.startsWith("file")) {
managerLocal
lazyLocalDS()
} else if (fileName.endsWith("m3u8")) {
managerHls
lazyHlsDS()
} else {
managerProgressive
lazyProgressiveDS()
}
}
@@ -49,8 +49,8 @@ fun getLanguagesSpokenByUser(): Set<String> {
return codedList
}
val GLOBAL_FOLLOWS = " Global "
val KIND3_FOLLOWS = " All Follows "
val GLOBAL_FOLLOWS = " Global " // This has spaces to avoid mixing with a potential NIP-51 list with the same name.
val KIND3_FOLLOWS = " All Follows " // This has spaces to avoid mixing with a potential NIP-51 list with the same name.
@OptIn(DelicateCoroutinesApi::class)
@Stable
@@ -492,7 +492,13 @@ class Account(
fun broadcast(note: Note) {
note.event?.let {
Client.send(it)
if (it is WrappedEvent && it.host != null) {
it.host?.let { hostEvent ->
Client.send(hostEvent)
}
} else {
Client.send(it)
}
}
}
@@ -908,6 +914,23 @@ class Account(
Client.send(signedEvent, relayList = relayList)
LocalCache.consume(signedEvent)
// broadcast replied notes
replyingTo?.let {
LocalCache.getNoteIfExists(replyingTo)?.event?.let {
Client.send(it, relayList = relayList)
}
}
replyTo?.forEach {
it.event?.let {
Client.send(it, relayList = relayList)
}
}
addresses?.forEach {
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
Client.send(it, relayList = relayList)
}
}
return null
}
@@ -958,6 +981,17 @@ class Account(
Client.send(signedEvent, relayList = relayList)
LocalCache.consume(signedEvent)
replyTo?.forEach {
it.event?.let {
Client.send(it, relayList = relayList)
}
}
addresses?.forEach {
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
Client.send(it, relayList = relayList)
}
}
return null
}
@@ -1800,8 +1834,8 @@ class Account(
}.toTypedArray()
}
fun convertGlobalRelays(): Array<String> {
return localRelays.filter { it.feedTypes.contains(FeedType.GLOBAL) }
fun activeGlobalRelays(): Array<String> {
return (activeRelays() ?: convertLocalRelays()).filter { it.activeTypes.contains(FeedType.GLOBAL) }
.map { it.url }
.toTypedArray()
}
@@ -1051,6 +1051,23 @@ object LocalCache {
refreshObservers(note)
}
fun consume(event: AudioHeaderEvent, relay: Relay?) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
fun consume(event: FileHeaderEvent, relay: Relay?) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
@@ -1284,7 +1301,7 @@ object LocalCache {
}
return notes.values.filter {
(it.event !is GenericRepostEvent && it.event !is RepostEvent && it.event !is CommunityPostApprovalEvent && it.event !is ReactionEvent) &&
(it.event !is GenericRepostEvent && it.event !is RepostEvent && it.event !is CommunityPostApprovalEvent && it.event !is ReactionEvent && it.event !is GiftWrapEvent) &&
(
it.event?.content()?.contains(text, true) ?: false ||
it.event?.matchTag1With(text) ?: false ||
@@ -1292,7 +1309,7 @@ object LocalCache {
it.idNote().startsWith(text, true)
)
} + addressables.values.filter {
(it.event !is GenericRepostEvent && it.event !is RepostEvent && it.event !is CommunityPostApprovalEvent && it.event !is ReactionEvent) &&
(it.event !is GenericRepostEvent && it.event !is RepostEvent && it.event !is CommunityPostApprovalEvent && it.event !is ReactionEvent && it.event !is GiftWrapEvent) &&
(
it.event?.content()?.contains(text, true) ?: false ||
it.event?.matchTag1With(text) ?: false ||
@@ -1524,6 +1541,7 @@ object LocalCache {
is AdvertisedRelayListEvent -> consume(event)
is AppDefinitionEvent -> consume(event)
is AppRecommendationEvent -> consume(event)
is AudioHeaderEvent -> consume(event, relay)
is AudioTrackEvent -> consume(event)
is BadgeAwardEvent -> consume(event)
is BadgeDefinitionEvent -> consume(event)
@@ -129,12 +129,14 @@ open class Note(val idHex: String) {
return "/" + formattedDateTime(createdAt() ?: 0) + ";"
}
val mySignature = replyTo
.filter { it in eventsToConsider } // This forces the signature to be based on a branch, avoiding two roots
.map {
cachedSignatures[it] ?: it.replyLevelSignature(eventsToConsider, cachedSignatures).apply { cachedSignatures.put(it, this) }
}
.maxBy { it.length }.removeSuffix(";") + "/" + formattedDateTime(createdAt() ?: 0) + ";"
val mySignature = (
replyTo
.filter { it in eventsToConsider } // This forces the signature to be based on a branch, avoiding two roots
.map {
cachedSignatures[it] ?: it.replyLevelSignature(eventsToConsider, cachedSignatures).apply { cachedSignatures.put(it, this) }
}
.maxByOrNull { it.length }?.removeSuffix(";") ?: ""
) + "/" + formattedDateTime(createdAt() ?: 0) + ";"
cachedSignatures[this] = mySignature
return mySignature
@@ -613,6 +615,7 @@ open class Note(val idHex: String) {
fun clearLive() {
if (liveSet != null && liveSet?.isInUse() == false) {
liveSet?.destroy()
liveSet = null
}
}
@@ -654,12 +657,26 @@ class NoteLiveSet(u: Note) {
relays.hasObservers() ||
zaps.hasObservers()
}
fun destroy() {
metadata.destroy()
reactions.destroy()
boosts.destroy()
replies.destroy()
reports.destroy()
relays.destroy()
zaps.destroy()
}
}
class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(500, Dispatchers.IO)
fun destroy() {
bundler.cancel()
}
fun invalidateData() {
if (!hasObservers()) return
@@ -15,7 +15,12 @@ class ThreadAssembler {
testedNotes.add(note)
val markedAsRoot = note.event?.tags()?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1)
if (markedAsRoot != null) return LocalCache.checkGetOrCreateNote(markedAsRoot)
if (markedAsRoot != null) {
// Check to ssee if there is an error in the tag and the root has replies
if (LocalCache.getNoteIfExists(markedAsRoot)?.replyTo?.isEmpty() == true) {
return LocalCache.checkGetOrCreateNote(markedAsRoot)
}
}
val hasNoReplyTo = note.replyTo?.firstOrNull { it.replyTo?.isEmpty() == true }
if (hasNoReplyTo != null) return hasNoReplyTo
@@ -372,6 +372,7 @@ class User(val pubkeyHex: String) {
fun clearLive() {
if (liveSet != null && liveSet?.isInUse() == false) {
liveSet?.destroy()
liveSet = null
}
}
@@ -400,6 +401,18 @@ class UserLiveSet(u: User) {
zaps.hasObservers() ||
bookmarks.hasObservers()
}
fun destroy() {
follows.destroy()
followers.destroy()
reports.destroy()
messages.destroy()
relays.destroy()
relayInfo.destroy()
metadata.destroy()
zaps.destroy()
bookmarks.destroy()
}
}
@Immutable
@@ -470,6 +483,10 @@ class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(500, Dispatchers.IO)
fun destroy() {
bundler.cancel()
}
fun invalidateData() {
if (!hasObservers()) return
checkNotInMainThread()
@@ -76,6 +76,7 @@ abstract class NostrDataSource(val debugName: String) {
fun destroy() {
stop()
Client.unsubscribe(clientListener)
bundler.cancel()
}
open fun start() {
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
@@ -26,7 +27,7 @@ object NostrGeohashDataSource : NostrDataSource("SingleGeoHashFeed") {
hashToLoad
)
),
kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, LiveActivitiesChatMessageEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind),
kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, LiveActivitiesChatMessageEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, AudioHeaderEvent.kind),
limit = 200
)
)
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
@@ -29,7 +30,7 @@ object NostrHashtagDataSource : NostrDataSource("SingleHashtagFeed") {
hashToLoad.capitalize()
)
),
kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, LiveActivitiesChatMessageEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind),
kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, LiveActivitiesChatMessageEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, AudioHeaderEvent.kind),
limit = 200
)
)
@@ -6,6 +6,7 @@ import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
@@ -56,10 +57,10 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
val follows = account.selectedUsersFollowList(account.defaultHomeFollowList)
val followKeys = follows?.map {
it.substring(0, 6)
it.substring(0, 8)
}
val followSet = followKeys?.plus(account.userProfile().pubkeyHex.substring(0, 6))
val followSet = followKeys?.plus(account.userProfile().pubkeyHex.substring(0, 8))
return TypedFilter(
types = setOf(FeedType.FOLLOWS),
@@ -73,6 +74,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
PollNoteEvent.kind,
HighlightEvent.kind,
AudioTrackEvent.kind,
AudioHeaderEvent.kind,
PinListEvent.kind,
LiveActivitiesChatMessageEvent.kind,
LiveActivitiesEvent.kind
@@ -92,7 +94,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
return TypedFilter(
types = setOf(FeedType.FOLLOWS),
filter = JsonFilter(
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, PinListEvent.kind),
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioHeaderEvent.kind, AudioTrackEvent.kind, PinListEvent.kind),
tags = mapOf(
"t" to hashToLoad.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
@@ -112,7 +114,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
return TypedFilter(
types = setOf(FeedType.FOLLOWS),
filter = JsonFilter(
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, PinListEvent.kind),
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioHeaderEvent.kind, AudioTrackEvent.kind, PinListEvent.kind),
tags = mapOf(
"g" to hashToLoad.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
@@ -137,6 +139,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
LongTextNoteEvent.kind,
ClassifiedsEvent.kind,
HighlightEvent.kind,
AudioHeaderEvent.kind,
AudioTrackEvent.kind,
PinListEvent.kind,
CommunityPostApprovalEvent.kind
@@ -56,8 +56,9 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
filter = JsonFilter(
kinds = listOf(
TextNoteEvent.kind, LongTextNoteEvent.kind, BadgeDefinitionEvent.kind,
PeopleListEvent.kind, BookmarkListEvent.kind, AudioTrackEvent.kind, PinListEvent.kind,
PollNoteEvent.kind, ChannelCreateEvent.kind
PeopleListEvent.kind, BookmarkListEvent.kind, AudioHeaderEvent.kind,
AudioTrackEvent.kind, PinListEvent.kind, PollNoteEvent.kind,
ChannelCreateEvent.kind
),
search = mySearchString,
limit = 100
@@ -34,6 +34,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
RepostEvent.kind,
LongTextNoteEvent.kind,
AudioTrackEvent.kind,
AudioHeaderEvent.kind,
PinListEvent.kind,
PollNoteEvent.kind,
HighlightEvent.kind
@@ -20,7 +20,6 @@ import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
@@ -102,9 +101,9 @@ class MainActivity : AppCompatActivity() {
}
override fun onPause() {
if (BuildConfig.DEBUG) {
debugState(this)
}
// if (BuildConfig.DEBUG) {
debugState(this)
// }
ServiceManager.pause()
super.onPause()
@@ -241,6 +241,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
postViewModel.galleryUri?.let {
VideoView(
videoUri = it.toString(),
roundedCorner = false,
accountViewModel = accountViewModel
)
}
@@ -113,7 +113,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun NewPostView(
onClose: () -> Unit,
@@ -409,7 +408,7 @@ fun NewPostView(
)
)
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
VideoView(myUrlPreview, accountViewModel = accountViewModel)
VideoView(myUrlPreview, roundedCorner = true, accountViewModel = accountViewModel)
} else {
UrlPreview(myUrlPreview, myUrlPreview, accountViewModel)
}
@@ -1389,7 +1388,7 @@ fun ImageVideoDescription(
)
}
} else {
VideoView(uri.toString(), accountViewModel = accountViewModel)
VideoView(uri.toString(), roundedCorner = true, accountViewModel = accountViewModel)
}
}
@@ -48,7 +48,7 @@ import kotlinx.coroutines.withContext
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun NewImageButton(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
fun NewImageButton(accountViewModel: AccountViewModel, nav: (String) -> Unit, navScrollToTop: (Route, Boolean) -> Unit) {
var wantsToPost by remember {
mutableStateOf(false)
}
@@ -62,11 +62,9 @@ fun NewImageButton(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val postViewModel: NewMediaModel = viewModel()
postViewModel.onceUploaded {
scope.launch(Dispatchers.Default) {
// awaits an refresh on the list
delay(250)
delay(500)
withContext(Dispatchers.Main) {
val route = Route.Video.route.replace("{scrollToTop}", "true")
nav(route)
navScrollToTop(Route.Video, true)
}
}
}
@@ -0,0 +1,160 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.DrawStyle
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.coerceIn
import androidx.compose.ui.unit.dp
import com.linc.audiowaveform.model.AmplitudeType
import com.linc.audiowaveform.model.WaveformAlignment
import kotlin.math.ceil
import kotlin.math.roundToInt
private val MinSpikeWidthDp: Dp = 1.dp
private val MaxSpikeWidthDp: Dp = 24.dp
private val MinSpikePaddingDp: Dp = 0.dp
private val MaxSpikePaddingDp: Dp = 12.dp
private val MinSpikeRadiusDp: Dp = 0.dp
private val MaxSpikeRadiusDp: Dp = 12.dp
private const val MinProgress: Float = 0F
private const val MaxProgress: Float = 1F
private const val MinSpikeHeight: Float = 1F
private const val DefaultGraphicsLayerAlpha: Float = 0.99F
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun AudioWaveformReadOnly(
modifier: Modifier = Modifier,
style: DrawStyle = Fill,
waveformBrush: Brush = SolidColor(Color.White),
progressBrush: Brush = SolidColor(Color.Blue),
waveformAlignment: WaveformAlignment = WaveformAlignment.Center,
amplitudeType: AmplitudeType = AmplitudeType.Avg,
onProgressChangeFinished: (() -> Unit)? = null,
spikeAnimationSpec: AnimationSpec<Float> = tween(500),
spikeWidth: Dp = 3.dp,
spikeRadius: Dp = 2.dp,
spikePadding: Dp = 2.dp,
progress: Float = 0F,
amplitudes: List<Int>,
onProgressChange: (Float) -> Unit
) {
val backgroundColor = MaterialTheme.colors.background
val _progress = remember(progress) { progress.coerceIn(MinProgress, MaxProgress) }
val _spikeWidth = remember(spikeWidth) { spikeWidth.coerceIn(MinSpikeWidthDp, MaxSpikeWidthDp) }
val _spikePadding = remember(spikePadding) { spikePadding.coerceIn(MinSpikePaddingDp, MaxSpikePaddingDp) }
val _spikeRadius = remember(spikeRadius) { spikeRadius.coerceIn(MinSpikeRadiusDp, MaxSpikeRadiusDp) }
val _spikeTotalWidth = remember(spikeWidth, spikePadding) { _spikeWidth + _spikePadding }
var canvasSize by remember { mutableStateOf(Size(0f, 0f)) }
var spikes by remember { mutableStateOf(0F) }
val spikesAmplitudes = remember(amplitudes, spikes, amplitudeType) {
amplitudes.toDrawableAmplitudes(
amplitudeType = amplitudeType,
spikes = spikes.toInt(),
minHeight = MinSpikeHeight,
maxHeight = canvasSize.height.coerceAtLeast(MinSpikeHeight)
)
}.map { animateFloatAsState(it, spikeAnimationSpec).value }
Canvas(
modifier = Modifier
.fillMaxWidth()
.requiredHeight(48.dp)
.graphicsLayer(alpha = DefaultGraphicsLayerAlpha)
.then(modifier)
) {
canvasSize = size
spikes = size.width / _spikeTotalWidth.toPx()
spikesAmplitudes.forEachIndexed { index, amplitude ->
drawRoundRect(
brush = waveformBrush,
topLeft = Offset(
x = index * _spikeTotalWidth.toPx(),
y = when (waveformAlignment) {
WaveformAlignment.Top -> 0F
WaveformAlignment.Bottom -> size.height - amplitude
WaveformAlignment.Center -> size.height / 2F - amplitude / 2F
}
),
size = Size(
width = _spikeWidth.toPx(),
height = amplitude
),
cornerRadius = CornerRadius(_spikeRadius.toPx(), _spikeRadius.toPx()),
style = style
)
drawRect(
brush = progressBrush,
size = Size(
width = _progress * size.width,
height = size.height
),
blendMode = BlendMode.SrcAtop
)
}
}
}
private fun List<Int>.toDrawableAmplitudes(
amplitudeType: AmplitudeType,
spikes: Int,
minHeight: Float,
maxHeight: Float
): List<Float> {
val amplitudes = map(Int::toFloat)
if (amplitudes.isEmpty() || spikes == 0) {
return List(spikes) { minHeight }
}
val transform = { data: List<Float> ->
when (amplitudeType) {
AmplitudeType.Avg -> data.average()
AmplitudeType.Max -> data.max()
AmplitudeType.Min -> data.min()
}.toFloat().coerceIn(minHeight, maxHeight)
}
return when {
spikes > amplitudes.count() -> amplitudes.fillToSize(spikes, transform)
else -> amplitudes.chunkToSize(spikes, transform)
}.normalize(minHeight, maxHeight)
}
internal fun <T> Iterable<T>.fillToSize(size: Int, transform: (List<T>) -> T): List<T> {
val capacity = ceil(size.safeDiv(count())).roundToInt()
return map { data -> List(capacity) { data } }.flatten().chunkToSize(size, transform)
}
internal fun <T> Iterable<T>.chunkToSize(size: Int, transform: (List<T>) -> T): List<T> {
val chunkSize = count() / size
val remainder = count() % size
val remainderIndex = ceil(count().safeDiv(remainder)).roundToInt()
val chunkIteration = filterIndexed { index, _ ->
remainderIndex == 0 || index % remainderIndex != 0
}.chunked(chunkSize, transform)
return when (size) {
chunkIteration.count() -> chunkIteration
else -> chunkIteration.chunkToSize(size, transform)
}
}
internal fun Iterable<Float>.normalize(min: Float, max: Float): List<Float> {
return map { (max - min) * ((it - min()) / (max() - min())) + min }
}
private fun Int.safeDiv(value: Int): Float {
return if (value == 0) return 0F else this / value.toFloat()
}
@@ -7,6 +7,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -21,6 +23,8 @@ class BundledUpdate(
val delay: Long,
val dispatcher: CoroutineDispatcher = Dispatchers.Default
) {
val scope = CoroutineScope(dispatcher + SupervisorJob())
private var onlyOneInBlock = AtomicBoolean()
private var invalidatesAgain = false
@@ -32,7 +36,6 @@ class BundledUpdate(
return
}
val scope = CoroutineScope(Job() + dispatcher)
scope.launch {
try {
onUpdate()
@@ -48,6 +51,10 @@ class BundledUpdate(
}
}
}
fun cancel() {
scope.cancel()
}
}
/**
@@ -58,6 +65,8 @@ class BundledInsert<T>(
val delay: Long,
val dispatcher: CoroutineDispatcher = Dispatchers.Default
) {
val scope = CoroutineScope(dispatcher + SupervisorJob())
private var onlyOneInBlock = AtomicBoolean()
private var queue = LinkedBlockingQueue<T>()
@@ -88,4 +97,8 @@ class BundledInsert<T>(
}
}
}
fun cancel() {
scope.cancel()
}
}
@@ -287,7 +287,7 @@ private fun ZoomableContentView(
) {
state.imagesForPager[word]?.let {
Box(modifier = HalfVertPadding) {
ZoomableContentView(it, state.imageList, accountViewModel)
ZoomableContentView(it, state.imageList, roundedCorner = true, accountViewModel)
}
}
}
@@ -9,6 +9,8 @@ import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.annotation.OptIn
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
@@ -17,6 +19,7 @@ import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
@@ -34,6 +37,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
@@ -54,6 +59,7 @@ import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView
import coil.imageLoader
import coil.request.ImageRequest
import com.linc.audiowaveform.infiniteLinearGradient
import com.vitorpamplona.amethyst.PlaybackClientController
import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
@@ -66,8 +72,12 @@ import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize
import com.vitorpamplona.amethyst.ui.theme.imageModifier
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import java.util.UUID
import kotlin.math.abs
@@ -80,6 +90,7 @@ fun LoadThumbAndThenVideoView(
title: String? = null,
thumbUri: String,
authorName: String? = null,
roundedCorner: Boolean,
nostrUriCallback: String? = null,
accountViewModel: AccountViewModel,
onDialog: ((Boolean) -> Unit)? = null
@@ -111,6 +122,7 @@ fun LoadThumbAndThenVideoView(
videoUri = videoUri,
title = title,
thumb = VideoThumb(loadingFinished.second),
roundedCorner = roundedCorner,
artworkUri = thumbUri,
authorName = authorName,
nostrUriCallback = nostrUriCallback,
@@ -122,6 +134,7 @@ fun LoadThumbAndThenVideoView(
videoUri = videoUri,
title = title,
thumb = null,
roundedCorner = roundedCorner,
artworkUri = thumbUri,
authorName = authorName,
nostrUriCallback = nostrUriCallback,
@@ -137,6 +150,8 @@ fun VideoView(
videoUri: String,
title: String? = null,
thumb: VideoThumb? = null,
roundedCorner: Boolean,
waveform: ImmutableList<Int>? = null,
artworkUri: String? = null,
authorName: String? = null,
nostrUriCallback: String? = null,
@@ -151,6 +166,8 @@ fun VideoView(
defaultToStart,
title,
thumb,
roundedCorner,
waveform,
artworkUri,
authorName,
nostrUriCallback,
@@ -167,6 +184,8 @@ fun VideoViewInner(
defaultToStart: Boolean = false,
title: String? = null,
thumb: VideoThumb? = null,
roundedCorner: Boolean,
waveform: ImmutableList<Int>? = null,
artworkUri: String? = null,
authorName: String? = null,
nostrUriCallback: String? = null,
@@ -220,7 +239,16 @@ fun VideoViewInner(
defaultToStart = defaultToStart,
nostrUriCallback = nostrUriCallback
) { controller, keepPlaying ->
RenderVideoPlayer(controller, thumb, keepPlaying, automaticallyStartPlayback, activeOnScreen, onDialog)
RenderVideoPlayer(
controller = controller,
thumbData = thumb,
roundedCorner = roundedCorner,
waveform = waveform,
keepPlaying = keepPlaying,
automaticallyStartPlayback = automaticallyStartPlayback,
activeOnScreen = activeOnScreen,
onDialog = onDialog
)
}
}
}
@@ -472,6 +500,8 @@ data class VideoThumb(
private fun RenderVideoPlayer(
controller: MediaController,
thumbData: VideoThumb?,
roundedCorner: Boolean,
waveform: ImmutableList<Int>? = null,
keepPlaying: MutableState<Boolean>,
automaticallyStartPlayback: MutableState<Boolean>,
activeOnScreen: MutableState<Boolean>,
@@ -487,10 +517,15 @@ private fun RenderVideoPlayer(
BoxWithConstraints() {
AndroidView(
modifier = Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 70.dp)
.align(Alignment.Center),
modifier = if (roundedCorner) {
MaterialTheme.colors.imageModifier
.defaultMinSize(minHeight = 100.dp)
.align(Alignment.Center)
} else {
Modifier.fillMaxWidth()
.defaultMinSize(minHeight = 100.dp)
.align(Alignment.Center)
},
factory = {
PlayerView(context).apply {
player = controller
@@ -518,6 +553,10 @@ private fun RenderVideoPlayer(
}
)
waveform?.let {
Waveform(it, controller, Modifier.align(Alignment.Center))
}
val startingMuteState = remember(controller) {
controller.volume < 0.001
}
@@ -555,6 +594,69 @@ private fun RenderVideoPlayer(
}
}
private fun pollCurrentDuration(controller: MediaController) = flow {
while (controller.currentPosition <= controller.duration) {
emit(controller.currentPosition / controller.duration.toFloat())
delay(100)
}
}.conflate()
@Composable
fun Waveform(
waveform: ImmutableList<Int>,
controller: MediaController,
modifier: Modifier
) {
val waveformProgress = remember { mutableStateOf(0F) }
DrawWaveform(waveform, waveformProgress, modifier)
val restartFlow = remember {
mutableStateOf(0)
}
// Keeps the screen on while playing and viewing videos.
DisposableEffect(key1 = controller) {
val listener = object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
// doesn't consider the mutex because the screen can turn off if the video
// being played in the mutex is not visible.
if (isPlaying) {
restartFlow.value += 1
}
}
}
controller.addListener(listener)
onDispose {
controller.removeListener(listener)
}
}
LaunchedEffect(key1 = restartFlow.value) {
pollCurrentDuration(controller).collect() { value ->
waveformProgress.value = value
}
}
}
@Composable
fun DrawWaveform(waveform: ImmutableList<Int>, waveformProgress: MutableState<Float>, modifier: Modifier) {
AudioWaveformReadOnly(
modifier = modifier.padding(start = 10.dp, end = 10.dp),
amplitudes = waveform,
progress = waveformProgress.value,
progressBrush = Brush.infiniteLinearGradient(
colors = listOf(Color(0xff2598cf), Color(0xff652d80)),
animation = tween(durationMillis = 6000, easing = LinearEasing),
width = 128F
),
onProgressChange = {
waveformProgress.value = it
}
)
}
@Composable
fun ControlWhenPlayerIsActive(
controller: Player,
@@ -29,11 +29,18 @@ import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
@@ -46,10 +53,12 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
@@ -77,11 +86,15 @@ import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
import com.vitorpamplona.amethyst.ui.note.HashCheckFailedIcon
import com.vitorpamplona.amethyst.ui.note.HashCheckIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Font17SP
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20dp
import com.vitorpamplona.amethyst.ui.theme.Size24dp
import com.vitorpamplona.amethyst.ui.theme.Size30dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import kotlinx.collections.immutable.ImmutableList
@@ -181,26 +194,38 @@ fun figureOutMimeType(fullUrl: String): ZoomableContent {
fun ZoomableContentView(
content: ZoomableContent,
images: ImmutableList<ZoomableContent> = listOf(content).toImmutableList(),
roundedCorner: Boolean,
accountViewModel: AccountViewModel
) {
val clipboardManager = LocalClipboardManager.current
// store the dialog open or close state
var dialogOpen by remember {
mutableStateOf(false)
}
var mainImageModifier = MaterialTheme.colors.imageModifier
// store the dialog open or close state
val shareOpen = remember {
mutableStateOf(false)
}
if (shareOpen.value) {
ShareImageAction(shareOpen, content) { shareOpen.value = false }
}
var mainImageModifier = if (roundedCorner) {
MaterialTheme.colors.imageModifier
} else {
Modifier.fillMaxWidth()
}
if (content is ZoomableUrlContent) {
mainImageModifier = mainImageModifier.combinedClickable(
onClick = { dialogOpen = true },
onLongClick = { clipboardManager.setText(AnnotatedString(content.uri ?: content.url)) }
onLongClick = { shareOpen.value = true }
)
} else if (content is ZoomablePreloadedContent) {
mainImageModifier = mainImageModifier.combinedClickable(
onClick = { dialogOpen = true },
onLongClick = { clipboardManager.setText(AnnotatedString(content.uri)) }
onLongClick = { shareOpen.value = true }
)
} else {
mainImageModifier = mainImageModifier.clickable {
@@ -215,6 +240,7 @@ fun ZoomableContentView(
title = content.description,
artworkUri = content.artworkUri,
authorName = content.authorName,
roundedCorner = roundedCorner,
nostrUriCallback = content.uri,
onDialog = { dialogOpen = true },
accountViewModel = accountViewModel
@@ -228,6 +254,7 @@ fun ZoomableContentView(
title = content.description,
artworkUri = content.artworkUri,
authorName = content.authorName,
roundedCorner = roundedCorner,
nostrUriCallback = content.uri,
onDialog = { dialogOpen = true },
accountViewModel = accountViewModel
@@ -624,11 +651,11 @@ fun ZoomableImageDialog(
pagerState = pagerState,
itemsCount = allImages.size,
itemContent = { index ->
RenderImageOrVideo(allImages[index], accountViewModel)
RenderImageOrVideo(allImages[index], false, accountViewModel)
}
)
} else {
RenderImageOrVideo(imageUrl, accountViewModel)
RenderImageOrVideo(imageUrl, false, accountViewModel)
}
Row(
@@ -642,7 +669,10 @@ fun ZoomableImageDialog(
val myContent = allImages[pagerState.currentPage]
if (myContent is ZoomableUrlContent) {
SaveToGallery(url = myContent.url)
Row() {
CopyToClipboard(content = myContent)
SaveToGallery(url = myContent.url)
}
} else if (myContent is ZoomableLocalImage && myContent.localFile != null) {
SaveToGallery(
localFile = myContent.localFile,
@@ -656,7 +686,65 @@ fun ZoomableImageDialog(
}
@Composable
private fun RenderImageOrVideo(content: ZoomableContent, accountViewModel: AccountViewModel) {
private fun CopyToClipboard(
content: ZoomableContent
) {
val popupExpanded = remember { mutableStateOf(false) }
Button(
modifier = Modifier.padding(horizontal = Size5dp),
onClick = { popupExpanded.value = true },
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.placeholderText
)
) {
Icon(
tint = Color.White,
imageVector = Icons.Default.Share,
modifier = Size20Modifier,
contentDescription = stringResource(R.string.copy_url_to_clipboard)
)
ShareImageAction(popupExpanded, content) { popupExpanded.value = false }
}
}
@Composable
private fun ShareImageAction(
popupExpanded:
MutableState<Boolean>,
content: ZoomableContent,
onDismiss: () -> Unit
) {
DropdownMenu(
expanded = popupExpanded.value,
onDismissRequest = onDismiss
) {
val clipboardManager = LocalClipboardManager.current
if (content is ZoomableUrlContent) {
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(content.url)); onDismiss() }) {
Text(stringResource(R.string.copy_url_to_clipboard))
}
if (content.uri != null) {
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(content.uri)); onDismiss() }) {
Text(stringResource(R.string.copy_the_note_id_to_the_clipboard))
}
}
}
if (content is ZoomablePreloadedContent) {
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(content.uri)); onDismiss() }) {
Text(stringResource(R.string.copy_the_note_id_to_the_clipboard))
}
}
}
}
@Composable
private fun RenderImageOrVideo(content: ZoomableContent, roundedCorner: Boolean, accountViewModel: AccountViewModel) {
val mainModifier = Modifier
.fillMaxSize()
.zoomable(rememberZoomState())
@@ -670,6 +758,7 @@ private fun RenderImageOrVideo(content: ZoomableContent, accountViewModel: Accou
title = content.description,
artworkUri = content.artworkUri,
authorName = content.authorName,
roundedCorner = roundedCorner,
accountViewModel = accountViewModel,
alwaysShowVideo = true
)
@@ -684,6 +773,7 @@ private fun RenderImageOrVideo(content: ZoomableContent, accountViewModel: Accou
title = content.description,
artworkUri = content.artworkUri,
authorName = content.authorName,
roundedCorner = roundedCorner,
accountViewModel = accountViewModel,
alwaysShowVideo = true
)
@@ -11,13 +11,19 @@ import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_PLANNED
import com.vitorpamplona.quartz.utils.TimeUtils
open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
open class DiscoverLiveFeedFilter(
val account: Account
) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String {
return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList
return account.userProfile().pubkeyHex + "-" + followList()
}
open fun followList(): String {
return account.defaultDiscoveryFollowList
}
override fun showHiddenKey(): Boolean {
return account.defaultDiscoveryFollowList == PeopleListEvent.blockList
return followList() == PeopleListEvent.blockList
}
override fun feed(): List<Note> {
@@ -39,15 +45,15 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Not
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
val isHiddenList = showHiddenKey()
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
val followingGeohashSet = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
val followingKeySet = account.selectedUsersFollowList(followList()) ?: emptySet()
val followingTagSet = account.selectedTagsFollowList(followList()) ?: emptySet()
val followingGeohashSet = account.selectedGeohashesFollowList(followList()) ?: emptySet()
val activities = collection
.asSequence()
.filter { it.event is LiveActivitiesEvent }
.filter {
isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(
isGlobal || (it.event as LiveActivitiesEvent).participantsIntersect(followingKeySet) || it.event?.isTaggedHashes(
followingTagSet
) == true || it.event?.isTaggedGeoHashes(
followingGeohashSet
@@ -61,7 +67,7 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Not
}
override fun sort(collection: Set<Note>): List<Note> {
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
val followingKeySet = account.selectedUsersFollowList(followList())
val counter = ParticipantListBuilder()
val participantCounts = collection.associate {
@@ -1,12 +1,26 @@
package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.OnlineChecker
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE
class DiscoverLiveNowFeedFilter(account: Account) : DiscoverLiveFeedFilter(account) {
class DiscoverLiveNowFeedFilter(
account: Account
) : DiscoverLiveFeedFilter(account) {
override fun followList(): String {
// uses follows by default, but other lists if they were selected in the top bar
val currentList = super.followList()
return if (currentList == GLOBAL_FOLLOWS) {
KIND3_FOLLOWS
} else {
currentList
}
}
override fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val allItems = super.innerApplyFilter(collection)
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent
@@ -32,7 +33,8 @@ class GeoHashFeedFilter(val tag: String, val account: Account) : AdditiveFeedFil
it.event is TextNoteEvent ||
it.event is LongTextNoteEvent ||
it.event is ChannelMessageEvent ||
it.event is PrivateDmEvent
it.event is PrivateDmEvent ||
it.event is AudioHeaderEvent
) &&
it.event?.isTaggedGeoHash(myTag) == true
}
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent
@@ -32,7 +33,8 @@ class HashtagFeedFilter(val tag: String, val account: Account) : AdditiveFeedFil
it.event is TextNoteEvent ||
it.event is LongTextNoteEvent ||
it.event is ChannelMessageEvent ||
it.event is PrivateDmEvent
it.event is PrivateDmEvent ||
it.event is AudioHeaderEvent
) &&
it.event?.isTaggedHash(myTag) == true
}
@@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
@@ -37,7 +38,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
private fun innerApplyFilter(collection: Collection<Note>, ignoreAddressables: Boolean): Set<Note> {
val isGlobal = account.defaultHomeFollowList == GLOBAL_FOLLOWS
val gRelays = account.convertGlobalRelays()
val gRelays = account.activeGlobalRelays()
val isHiddenList = showHiddenKey()
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
@@ -53,7 +54,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
.filter { it ->
val noteEvent = it.event
val isGlobalRelay = it.relays.any { gRelays.contains(it) }
(noteEvent is TextNoteEvent || noteEvent is ClassifiedsEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent || noteEvent is LongTextNoteEvent || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || noteEvent is AudioTrackEvent) &&
(noteEvent is TextNoteEvent || noteEvent is ClassifiedsEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent || noteEvent is LongTextNoteEvent || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || noteEvent is AudioTrackEvent || noteEvent is AudioHeaderEvent) &&
(!ignoreAddressables || noteEvent.kind() < 10000) &&
((isGlobal && isGlobalRelay) || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet) || noteEvent.isTaggedGeoHashes(followingGeoSet) || noteEvent.isTaggedAddressableNotes(followingCommunities)) &&
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
@@ -40,7 +40,7 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
it.event !is BadgeDefinitionEvent &&
it.event !is BadgeProfilesEvent &&
it.event !is GiftWrapEvent &&
it.author !== loggedInUser &&
(it.event is LnZapEvent || it.author !== loggedInUser) &&
(isGlobal || it.author?.pubkeyHex in followingKeySet) &&
it.event?.isTaggedUser(loggedInUserHex) ?: false &&
(isHiddenList || it.author == null || !account.isHidden(it.author!!.pubkeyHex)) &&
@@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
@@ -41,7 +42,8 @@ class UserProfileNewThreadFeedFilter(val user: User, val account: Account) : Add
it.event is LongTextNoteEvent ||
it.event is PollNoteEvent ||
it.event is HighlightEvent ||
it.event is AudioTrackEvent
it.event is AudioTrackEvent ||
it.event is AudioHeaderEvent
) &&
it.isNewThread() &&
account.isAcceptable(it) == true
@@ -732,9 +732,9 @@ fun debugState(context: Context) {
Log.d("STATE DUMP", "Image Disk Cache ${(imageLoader.diskCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.diskCache?.maxSize ?: 0) / (1024 * 1024)} MB")
Log.d("STATE DUMP", "Image Memory Cache ${(imageLoader.memoryCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.memoryCache?.maxSize ?: 0) / (1024 * 1024)} MB")
Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.event != null }.size + "/" + LocalCache.notes.size)
Log.d("STATE DUMP", "Addressables: " + LocalCache.addressables.filter { it.value.event != null }.size + "/" + LocalCache.addressables.size)
Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + "/" + LocalCache.users.size)
Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.liveSet != null }.size + " / " + LocalCache.notes.filter { it.value.event != null }.size + "/" + LocalCache.notes.size)
Log.d("STATE DUMP", "Addressables: " + LocalCache.addressables.filter { it.value.liveSet != null }.size + " / " + LocalCache.addressables.filter { it.value.event != null }.size + "/" + LocalCache.addressables.size)
Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.liveSet != null }.size + " / " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + "/" + LocalCache.users.size)
Log.d("STATE DUMP", "Memory used by Events: " + LocalCache.notes.values.sumOf { it.event?.countMemory() ?: 0 } / (1024 * 1024) + " MB")
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@@ -58,6 +59,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.EndedFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size35dp
@@ -361,6 +363,10 @@ fun InnerChannelCardWithReactions(
)
}
}
Divider(
thickness = DividerThickness
)
}
@Composable
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -25,6 +26,7 @@ import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -118,5 +120,9 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
NoteDropDownMenu(baseNote, popupExpanded, accountViewModel)
}
}
Divider(
thickness = DividerThickness
)
}
}
@@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@@ -54,6 +55,7 @@ import com.vitorpamplona.amethyst.ui.screen.CombinedZap
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifier
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifierSmaller
import com.vitorpamplona.amethyst.ui.theme.Size10dp
@@ -159,6 +161,10 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, showHi
NoteDropDownMenu(baseNote, popupExpanded, accountViewModel)
}
Divider(
thickness = DividerThickness
)
}
}
@@ -154,6 +154,7 @@ import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.BadgeAwardEvent
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
@@ -201,7 +202,6 @@ import java.io.File
import java.math.BigDecimal
import java.net.URL
import java.util.Locale
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
@OptIn(ExperimentalFoundationApi::class)
@@ -480,8 +480,8 @@ fun NormalNote(
)
}
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
is FileHeaderEvent -> FileHeaderDisplay(baseNote, accountViewModel)
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, accountViewModel)
is FileHeaderEvent -> FileHeaderDisplay(baseNote, isQuotedNote, accountViewModel)
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, isQuotedNote, accountViewModel)
else ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
CheckNewAndRenderNote(
@@ -896,7 +896,6 @@ fun ClickableNote(
}
}
@OptIn(ExperimentalTime::class)
@Composable
fun InnerNoteWithReactions(
baseNote: Note,
@@ -1045,6 +1044,14 @@ private fun RenderNoteRow(
RenderAppDefinition(baseNote, accountViewModel, nav)
}
is AudioTrackEvent -> {
RenderAudioTrack(baseNote, accountViewModel, nav)
}
is AudioHeaderEvent -> {
RenderAudioHeader(baseNote, accountViewModel, nav)
}
is ReactionEvent -> {
RenderReaction(baseNote, backgroundColor, accountViewModel, nav)
}
@@ -1077,10 +1084,6 @@ private fun RenderNoteRow(
DisplayRelaySet(baseNote, backgroundColor, accountViewModel, nav)
}
is AudioTrackEvent -> {
RenderAudioTrack(baseNote, accountViewModel, nav)
}
is PinListEvent -> {
RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav)
}
@@ -2251,6 +2254,17 @@ private fun RenderAudioTrack(
AudioTrackHeader(noteEvent, note, accountViewModel, nav)
}
@Composable
private fun RenderAudioHeader(
note: Note,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val noteEvent = note.event as? AudioHeaderEvent ?: return
AudioHeader(noteEvent, note, accountViewModel, nav)
}
@Composable
private fun RenderLongFormContent(
note: Note,
@@ -3178,7 +3192,7 @@ private fun RenderBadge(
}
@Composable
fun FileHeaderDisplay(note: Note, accountViewModel: AccountViewModel) {
fun FileHeaderDisplay(note: Note, isQuotedNote: Boolean, accountViewModel: AccountViewModel) {
val event = (note.event as? FileHeaderEvent) ?: return
val fullUrl = event.url() ?: return
@@ -3214,18 +3228,18 @@ fun FileHeaderDisplay(note: Note, accountViewModel: AccountViewModel) {
}
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
ZoomableContentView(content = content, accountViewModel = accountViewModel)
ZoomableContentView(content = content, roundedCorner = isQuotedNote, accountViewModel = accountViewModel)
}
}
@Composable
fun FileStorageHeaderDisplay(baseNote: Note, accountViewModel: AccountViewModel) {
fun FileStorageHeaderDisplay(baseNote: Note, isQuotedNote: Boolean, accountViewModel: AccountViewModel) {
val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return
val dataEventId = eventHeader.dataEventId() ?: return
LoadNote(baseNoteHex = dataEventId) { contentNote ->
if (contentNote != null) {
ObserverAndRenderNIP95(baseNote, contentNote, accountViewModel)
ObserverAndRenderNIP95(baseNote, contentNote, isQuotedNote, accountViewModel)
}
}
}
@@ -3234,6 +3248,7 @@ fun FileStorageHeaderDisplay(baseNote: Note, accountViewModel: AccountViewModel)
private fun ObserverAndRenderNIP95(
header: Note,
content: Note,
isQuotedNote: Boolean,
accountViewModel: AccountViewModel
) {
val eventHeader = (header.event as? FileStorageHeaderEvent) ?: return
@@ -3280,7 +3295,7 @@ private fun ObserverAndRenderNIP95(
Crossfade(targetState = content) {
if (it != null) {
SensitivityWarning(note = header, accountViewModel = accountViewModel) {
ZoomableContentView(content = it, accountViewModel = accountViewModel)
ZoomableContentView(content = it, roundedCorner = isQuotedNote, accountViewModel = accountViewModel)
}
}
}
@@ -3343,8 +3358,7 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, accountViewModel: A
media?.let { media ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(10.dp)
verticalAlignment = Alignment.CenterVertically
) {
cover?.let { cover ->
LoadThumbAndThenVideoView(
@@ -3352,6 +3366,7 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, accountViewModel: A
title = noteEvent.subject(),
thumbUri = cover,
authorName = note.author?.toBestDisplayName(),
roundedCorner = true,
nostrUriCallback = "nostr:${note.toNEvent()}",
accountViewModel = accountViewModel
)
@@ -3360,6 +3375,7 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, accountViewModel: A
videoUri = media,
title = noteEvent.subject(),
authorName = note.author?.toBestDisplayName(),
roundedCorner = true,
accountViewModel = accountViewModel
)
}
@@ -3368,6 +3384,66 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, accountViewModel: A
}
}
@Composable
fun AudioHeader(noteEvent: AudioHeaderEvent, note: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val media = remember { noteEvent.stream() ?: noteEvent.download() }
val waveform = remember { noteEvent.wavefrom()?.toImmutableList()?.ifEmpty { null } }
val content = remember { noteEvent.content().ifBlank { null } }
val defaultBackground = MaterialTheme.colors.background
val background = remember { mutableStateOf(defaultBackground) }
val tags = remember(noteEvent) { noteEvent?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
val eventContent = remember(note.event) {
val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null }
val body = accountViewModel.decrypt(note)
if (!subject.isNullOrBlank() && body?.split("\n")?.get(0)?.contains(subject) == false) {
"## $subject\n$body"
} else {
body
}
}
Row(modifier = Modifier.padding(top = 5.dp)) {
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
media?.let { media ->
Row(
verticalAlignment = Alignment.CenterVertically
) {
VideoView(
videoUri = media,
waveform = waveform,
title = noteEvent.subject(),
authorName = note.author?.toBestDisplayName(),
roundedCorner = true,
accountViewModel = accountViewModel,
nostrUriCallback = note.toNostrUri()
)
}
}
content?.let {
Row(verticalAlignment = CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 5.dp)) {
TranslatableRichTextViewer(
content = it,
canPreview = true,
tags = tags,
backgroundColor = background,
accountViewModel = accountViewModel,
nav = nav
)
}
}
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
val hashtags = remember(noteEvent) { noteEvent.hashtags().toImmutableList() }
DisplayUncitedHashtags(hashtags, content ?: "", nav)
}
}
}
}
@Composable
fun RenderLiveActivityEvent(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
Row(modifier = Modifier.padding(top = 5.dp)) {
@@ -3486,6 +3562,7 @@ fun RenderLiveActivityEventInner(baseNote: Note, accountViewModel: AccountViewMo
title = subject,
artworkUri = cover,
authorName = baseNote.author?.toBestDisplayName(),
roundedCorner = true,
accountViewModel = accountViewModel,
nostrUriCallback = "nostr:${baseNote.toNEvent()}"
)
@@ -191,7 +191,7 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
takenIntoAccount.add(noteEvent.id())
}
} else if (noteEvent is LnZapEvent) {
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) {
if (noteEvent.isTaggedUser(currentUser)) { // the user might be sending his own receipts noteEvent.pubKey != currentUser
val netDate = formatDate(noteEvent.createdAt)
zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO)
takenIntoAccount.add(noteEvent.id())
@@ -245,7 +245,7 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
hasNewElements = true
}
} else if (noteEvent is LnZapEvent) {
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) {
if (noteEvent.isTaggedUser(currentUser)) { // && noteEvent.pubKey != currentUser User might be sending his own receipts
val netDate = formatDate(noteEvent.createdAt)
zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO)
takenIntoAccount.add(noteEvent.id())
@@ -326,6 +326,7 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
override fun onCleared() {
collectorJob?.cancel()
bundlerInsert.cancel()
super.onCleared()
}
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -22,6 +23,7 @@ import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.ui.screen.ZapUserSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
@@ -89,5 +91,9 @@ fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false,
UserCompose(baseUser = zapSetCard.user, accountViewModel = accountViewModel, nav = nav)
}
}
Divider(
thickness = DividerThickness
)
}
}
@@ -353,6 +353,8 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
override fun onCleared() {
clear()
bundlerInsert.cancel()
bundler.cancel()
collectorJob?.cancel()
super.onCleared()
}
@@ -340,6 +340,8 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), I
}
override fun onCleared() {
bundlerInsert.cancel()
bundler.cancel()
collectorJob?.cancel()
super.onCleared()
}
@@ -96,6 +96,7 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter<ZapReqResponse>) : View
}
override fun onCleared() {
bundler.cancel()
collectorJob?.cancel()
super.onCleared()
}
@@ -91,6 +91,11 @@ class RelayFeedViewModel : ViewModel() {
refreshSuspended()
}
}
override fun onCleared() {
bundler.cancel()
super.onCleared()
}
}
@OptIn(ExperimentalMaterialApi::class)
@@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.selectedNote
import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
import com.vitorpamplona.quartz.events.ChannelCreateEvent
@@ -376,13 +377,15 @@ fun NoteMaster(
nav = nav
)
} else if (noteEvent is FileHeaderEvent) {
FileHeaderDisplay(baseNote, accountViewModel)
FileHeaderDisplay(baseNote, true, accountViewModel)
} else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(baseNote, accountViewModel)
FileStorageHeaderDisplay(baseNote, true, accountViewModel)
} else if (noteEvent is PeopleListEvent) {
DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
} else if (noteEvent is AudioTrackEvent) {
AudioTrackHeader(noteEvent, baseNote, accountViewModel, nav)
} else if (noteEvent is AudioHeaderEvent) {
AudioHeader(noteEvent, baseNote, accountViewModel, nav)
} else if (noteEvent is CommunityPostApprovalEvent) {
RenderPostApproval(
baseNote,
@@ -125,6 +125,7 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel(), In
}
override fun onCleared() {
bundler.cancel()
collectorJob?.cancel()
super.onCleared()
}
@@ -644,6 +644,7 @@ fun ShowVideoStreaming(
ZoomableContentView(
content = zoomableUrlVideo,
roundedCorner = false,
accountViewModel = accountViewModel
)
}
@@ -219,7 +219,7 @@ fun MainScreen(
}
},
floatingActionButton = {
FloatingButtons(navState, accountViewModel, accountStateViewModel, nav)
FloatingButtons(navState, accountViewModel, accountStateViewModel, nav, navBottomRow)
},
scaffoldState = scaffoldState
) {
@@ -249,7 +249,8 @@ fun FloatingButtons(
navEntryState: State<NavBackStackEntry?>,
accountViewModel: AccountViewModel,
accountStateViewModel: AccountStateViewModel,
nav: (String) -> Unit
nav: (String) -> Unit,
navScrollToTop: (Route, Boolean) -> Unit
) {
val accountState by accountStateViewModel.accountContent.collectAsState()
val context = LocalContext.current
@@ -265,7 +266,7 @@ fun FloatingButtons(
// Does nothing.
}
is AccountState.LoggedIn -> {
WritePermissionButtons(navEntryState, accountViewModel, nav)
WritePermissionButtons(navEntryState, accountViewModel, nav, navScrollToTop)
}
}
}
@@ -275,7 +276,8 @@ fun FloatingButtons(
private fun WritePermissionButtons(
navEntryState: State<NavBackStackEntry?>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
nav: (String) -> Unit,
navScrollToTop: (Route, Boolean) -> Unit
) {
val currentRoute by remember(navEntryState.value) {
derivedStateOf {
@@ -286,7 +288,7 @@ private fun WritePermissionButtons(
when (currentRoute) {
Route.Home.base -> NewNoteButton(accountViewModel, nav)
Route.Message.base -> ChannelFabColumn(accountViewModel, nav)
Route.Video.base -> NewImageButton(accountViewModel, nav)
Route.Video.base -> NewImageButton(accountViewModel, nav, navScrollToTop)
Route.Community.base -> {
val communityId by remember(navEntryState.value) {
derivedStateOf {
@@ -690,9 +690,6 @@ private fun ProfileHeader(
) {
MessageButton(baseUser, accountViewModel, nav)
// No need for this button anymore
// NPubCopyButton(baseUser)
ProfileActions(baseUser, accountViewModel)
}
}
@@ -195,6 +195,11 @@ class SearchBarViewModel(val account: Account) : ViewModel() {
}
}
override fun onCleared() {
bundler.cancel()
super.onCleared()
}
fun isSearchingFun() = searchValue.isNotBlank()
class Factory(val account: Account) : ViewModelProvider.Factory {
@@ -302,9 +302,9 @@ private fun RenderVideoOrPictureNote(
Row(remember { Modifier.weight(1f) }, verticalAlignment = Alignment.CenterVertically) {
val noteEvent = remember { note.event }
if (noteEvent is FileHeaderEvent) {
FileHeaderDisplay(note, accountViewModel)
FileHeaderDisplay(note, false, accountViewModel)
} else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(note, accountViewModel)
FileStorageHeaderDisplay(note, false, accountViewModel)
}
}
}
+4 -1
View File
@@ -103,7 +103,7 @@
<string name="uploading">Uploading…</string>
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">User does not have a lightning address setup to receive sats</string>
<string name="reply_here">"reply here.. "</string>
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Copies the Note ID to the clipboard for sharing</string>
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Copies the Note ID to the clipboard for sharing in Nostr</string>
<string name="copy_channel_id_note_to_the_clipboard">Copy Channel ID (Note) to the Clipboard</string>
<string name="edits_the_channel_metadata">Edits the Channel Metadata</string>
<string name="join">Join</string>
@@ -545,4 +545,7 @@
<string name="automatically_play_videos_description">Automatically plays videos and GIFs</string>
<string name="automatically_show_url_preview_description">Show URL previews</string>
<string name="load_image_description">When to load images</string>
<string name="copy_url_to_clipboard">Copy URL to clipboard</string>
<string name="copy_the_note_id_to_the_clipboard">Copy Note ID to clipboard</string>
</resources>