Merge branch 'vitorpamplona:main' into profiles-list-management

This commit is contained in:
KotlinGeekDev
2025-08-20 21:54:12 +00:00
committed by GitHub
86 changed files with 646 additions and 248 deletions
@@ -75,9 +75,10 @@ class Amethyst : Application() {
// App services that should be run as soon as there are subscribers to their flows
val locationManager = LocationState(this, applicationIOScope)
val torManager = TorManager(this, applicationIOScope)
val connManager = ConnectivityManager(this, applicationIOScope)
val torManager = TorManager(this, applicationIOScope)
// Service that will run at all times to receive events from Pokey
val pokeyReceiver = PokeyReceiver()
@@ -109,7 +110,7 @@ class Amethyst : Application() {
val client: NostrClient = NostrClient(websocketBuilder, applicationIOScope)
// Watches for changes on Tor and Relay List Settings
val relayProxyClientConnector = RelayProxyClientConnector(torProxySettingsAnchor, okHttpClients, connManager, client, applicationIOScope)
val relayProxyClientConnector = RelayProxyClientConnector(torProxySettingsAnchor, okHttpClients, connManager, client, torManager, applicationIOScope)
// Verifies and inserts in the cache from all relays, all subscriptions
val cacheClientConnector = CacheClientConnector(client, cache)
@@ -160,7 +160,6 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
import com.vitorpamplona.quartz.nip37Drafts.DraftBuilder
import com.vitorpamplona.quartz.nip37Drafts.DraftEventCache
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
@@ -185,9 +184,9 @@ import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip68Picture.PictureMeta
import com.vitorpamplona.quartz.nip68Picture.pictureIMeta
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoMeta
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
import com.vitorpamplona.quartz.nip71Video.VideoShortEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
import com.vitorpamplona.quartz.nip92IMeta.imetas
@@ -215,8 +214,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import java.util.Locale
import kotlin.collections.forEach
import kotlin.collections.ifEmpty
@OptIn(DelicateCoroutinesApi::class)
@Stable
@@ -985,7 +982,7 @@ class Account(
mimeType: String?,
hash: String,
duration: Int,
waveform: List<Int>,
waveform: List<Float>,
) {
signAndComputeBroadcast(VoiceEvent.build(url, mimeType, hash, duration, waveform))
}
@@ -995,7 +992,7 @@ class Account(
mimeType: String?,
hash: String,
duration: Int,
waveform: List<Int>,
waveform: List<Float>,
replyTo: EventHintBundle<VoiceEvent>,
) {
signAndComputeBroadcast(VoiceReplyEvent.build(url, mimeType, hash, duration, waveform, replyTo))
@@ -1088,11 +1085,11 @@ class Account(
)
if (headerInfo.dim.height > headerInfo.dim.width) {
VideoVerticalEvent.build(videoMeta, alt ?: "") {
VideoShortEvent.build(videoMeta, alt ?: "") {
contentWarningReason?.let { contentWarning(contentWarningReason) }
}
} else {
VideoHorizontalEvent.build(videoMeta, alt ?: "") {
VideoNormalEvent.build(videoMeta, alt ?: "") {
contentWarningReason?.let { contentWarning(contentWarningReason) }
}
}
@@ -1169,7 +1166,7 @@ class Account(
val extraRelays = cache.getAddressableNoteIfExists(DraftWrapEvent.createAddressTag(signer.pubKey, draftTag))?.relays ?: emptyList()
val rumor = RumorAssembler.assembleRumor(signer.pubKey, template)
val draftEvent = DraftBuilder.encryptAndSign(draftTag, rumor, signer)
val draftEvent = DraftWrapEvent.create(draftTag, rumor, signer)
draftsDecryptionCache.preload(draftEvent, rumor)
cache.justConsumeMyOwnEvent(draftEvent)
@@ -161,6 +161,8 @@ import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
import com.vitorpamplona.quartz.nip71Video.VideoShortEvent
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
@@ -852,7 +854,7 @@ object LocalCache : ILocalCache {
val creator = event.host()?.let { checkGetOrCreateUser(it.pubKey) } ?: author
channel.updateChannelInfo(creator, event)
channel.updateChannelInfo(creator, event, note)
refreshNewNoteObservers(note)
@@ -1036,6 +1038,18 @@ object LocalCache : ILocalCache {
wasVerified: Boolean,
) = consumeBaseReplaceable(event, relay, wasVerified)
private fun consume(
event: VideoNormalEvent,
relay: NormalizedRelayUrl?,
wasVerified: Boolean,
) = consumeBaseReplaceable(event, relay, wasVerified)
private fun consume(
event: VideoShortEvent,
relay: NormalizedRelayUrl?,
wasVerified: Boolean,
) = consumeBaseReplaceable(event, relay, wasVerified)
fun consume(
event: StatusEvent,
relay: NormalizedRelayUrl?,
@@ -1548,7 +1562,7 @@ object LocalCache : ILocalCache {
if (oldChannel.creator == null || oldChannel.creator == author) {
if (isVerified || justVerify(event)) {
oldChannel.updateChannelInfo(author, event)
oldChannel.updateChannelInfo(author, event, note)
}
}
@@ -1567,6 +1581,8 @@ object LocalCache : ILocalCache {
val oldChannel = checkGetOrCreatePublicChatChannel(channelId) ?: return false
val author = getOrCreateUser(event.pubKey)
val note = getOrCreateNote(event.id)
val isVerified =
if (event.createdAt > oldChannel.updatedMetadataAt) {
if (wasVerified || justVerify(event)) {
@@ -1579,7 +1595,6 @@ object LocalCache : ILocalCache {
wasVerified
}
val note = getOrCreateNote(event.id)
if (note.event == null && (isVerified || justVerify(event))) {
oldChannel.addNote(note, relay)
note.loadEvent(event, author, emptyList())
@@ -2821,7 +2836,9 @@ object LocalCache : ILocalCache {
is TorrentCommentEvent -> consume(event, relay, wasVerified)
is TrustedRelayListEvent -> consume(event, relay, wasVerified)
is VideoHorizontalEvent -> consume(event, relay, wasVerified)
is VideoNormalEvent -> consume(event, relay, wasVerified)
is VideoVerticalEvent -> consume(event, relay, wasVerified)
is VideoShortEvent -> consume(event, relay, wasVerified)
is VoiceEvent -> consume(event, relay, wasVerified)
is VoiceReplyEvent -> consume(event, relay, wasVerified)
is WikiNoteEvent -> consume(event, relay, wasVerified)
@@ -201,16 +201,19 @@ open class Note(
}
fun relayHintUrl(): NormalizedRelayUrl? {
val authorRelay = author?.latestMetadataRelay
val currentOutbox = author?.outboxRelays()?.toSet()
return if (relays.isNotEmpty()) {
if (authorRelay != null && relays.any { it == authorRelay }) {
authorRelay
} else {
relays.firstOrNull()
if (currentOutbox != null && currentOutbox.isNotEmpty()) {
val relayMatchesOutbox = relays.firstOrNull { it in currentOutbox }
if (relayMatchesOutbox != null) {
return relayMatchesOutbox
}
}
return relays.firstOrNull()
} else {
null
currentOutbox?.firstOrNull() ?: author?.latestMetadataRelay
}
}
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.model.nip28PublicChats
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
@@ -39,6 +40,7 @@ class PublicChatChannel(
) : Channel() {
var creator: User? = null
var event: ChannelCreateEvent? = null
var eventNote: Note? = null
var info = ChannelDataNorm(null, null, null, null)
var infoTags = EmptyTagList
@@ -61,13 +63,15 @@ class PublicChatChannel(
fun updateChannelInfo(
creator: User,
event: ChannelCreateEvent,
eventNote: Note? = null,
) {
this.creator = creator
this.event = event
this.info = event.channelInfo()
this.infoTags = event.tags.toImmutableListOfLists()
this.updatedMetadataAt = event.createdAt
this.eventNote = eventNote
updateChannelInfo()
}
@@ -77,8 +81,8 @@ class PublicChatChannel(
event: ChannelMetadataEvent,
) {
this.creator = creator
this.info = event.channelInfo()
this.infoTags = event.tags.toImmutableListOfLists()
this.updatedMetadataAt = event.createdAt
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.model.nip53LiveActivities
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
@@ -35,6 +36,7 @@ class LiveActivitiesChannel(
) : Channel() {
var creator: User? = null
var info: LiveActivitiesEvent? = null
var infoNote: Note? = null
fun address() = address
@@ -47,9 +49,11 @@ class LiveActivitiesChannel(
fun updateChannelInfo(
creator: User,
channelInfo: LiveActivitiesEvent,
channelInfoNote: Note,
) {
this.info = channelInfo
this.creator = creator
this.infoNote = channelInfoNote
super.updateChannelInfo()
}
@@ -63,6 +63,12 @@ class ConnectivityFlow(
Log.d("ConnectivityFlow", "onCapabilitiesChanged ${network.networkHandle} $isMobile")
trySend(ConnectivityStatus.Active(network.networkHandle, isMobile))
}
override fun onLost(network: Network) {
super.onLost(network)
Log.d("ConnectivityFlow", "onLost ${network.networkHandle} ")
trySend(ConnectivityStatus.Off)
}
}
connectivityManager.registerDefaultNetworkCallback(networkCallback)
@@ -47,7 +47,7 @@ import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
@Immutable
class WaveformData(
val wave: List<Int>,
val wave: List<Float>,
)
@Composable
@@ -21,14 +21,20 @@
package com.vitorpamplona.amethyst.service.relayClient
import android.util.Log
import com.vitorpamplona.amethyst.model.torState.TorRelayEvaluation
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityStatus
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
import com.vitorpamplona.amethyst.service.okhttp.ProxySettingsAnchor
import com.vitorpamplona.amethyst.ui.tor.TorManager
import com.vitorpamplona.amethyst.ui.tor.TorServiceStatus
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.NonCancellable.isActive
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flowOn
@@ -36,14 +42,24 @@ import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import net.freehaven.tor.control.TorControlCommands
import okhttp3.OkHttpClient
class RelayProxyClientConnector(
val torProxySettingsAnchor: ProxySettingsAnchor,
val okHttpClients: DualHttpClientManager,
val connManager: ConnectivityManager,
val client: NostrClient,
val torManager: TorManager,
val scope: CoroutineScope,
) {
data class RelayServiceInfra(
val torSettings: StateFlow<TorRelayEvaluation>,
val torConnection: OkHttpClient,
val clearConnection: OkHttpClient,
val connectivity: ConnectivityStatus,
)
@OptIn(FlowPreview::class)
val relayServices =
combine(
@@ -52,11 +68,35 @@ class RelayProxyClientConnector(
okHttpClients.defaultHttpClientWithoutProxy,
connManager.status,
) { torSettings, torConnection, clearConnection, connectivity ->
torSettings.hashCode() + torConnection.hashCode() + clearConnection.hashCode() + connectivity.hashCode()
RelayServiceInfra(torSettings, torConnection, clearConnection, connectivity)
}.debounce(100)
.onEach {
Log.d("ManageRelayServices", "Relay Services have changed")
client.reconnect(true)
if (it.connectivity is ConnectivityStatus.Off) {
Log.d("ManageRelayServices", "Pausing Relay Services ${it.connectivity}")
if (client.isActive()) {
client.disconnect()
}
val torStatus = torManager.status.value
if (torStatus is TorServiceStatus.Active) {
torStatus.torControlConnection.signal(TorControlCommands.SIGNAL_DORMANT)
Log.d("ManageRelayServices", "Pausing Tor Activity")
}
} else if (it.connectivity is ConnectivityStatus.Active && !client.isActive()) {
Log.d("ManageRelayServices", "Resuming Relay Services")
val torStatus = torManager.status.value
if (torStatus is TorServiceStatus.Active) {
torStatus.torControlConnection.signal(TorControlCommands.SIGNAL_ACTIVE)
torStatus.torControlConnection.signal(TorControlCommands.SIGNAL_NEWNYM)
Log.d("ManageRelayServices", "Resuming Tor Activity with new nym")
}
// only calls this if the client is not active. Otherwise goes to the else below
client.connect()
} else {
Log.d("ManageRelayServices", "Relay Services have changed, reconnecting relays that need to")
client.reconnect(true)
}
}.onStart {
Log.d("ManageRelayServices", "Resuming Relay Services")
client.connect()
@@ -61,7 +61,7 @@ fun NewUserMetadataScreen(
postViewModel.init(accountViewModel)
val context = LocalContext.current
LaunchedEffect(accountViewModel) {
LaunchedEffect(postViewModel, accountViewModel) {
postViewModel.load()
}
@@ -35,7 +35,7 @@ import java.io.File
class RecordingResult(
val file: File,
val mimeType: String,
val amplitudes: List<Int>,
val amplitudes: List<Float>,
val duration: Int,
)
@@ -44,7 +44,7 @@ class VoiceMessageRecorder {
private var outputFile: File? = null
private var startTime: Long = 0
private var job: Job? = null
private var amplitudes: MutableList<Int> = mutableListOf()
private var amplitudes: MutableList<Float> = mutableListOf()
private fun createRecorder(context: Context): MediaRecorder =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -65,7 +65,9 @@ class VoiceMessageRecorder {
this.amplitudes.clear()
createRecorder(context).apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setAudioEncodingBitRate(16 * 44100)
setAudioSamplingRate(44100) // Set the desired audio sampling rate (e.g., 44.1 kHz)
setAudioSource(MediaRecorder.AudioSource.VOICE_RECOGNITION)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
setOutputFile(outputFile)
@@ -80,7 +82,7 @@ class VoiceMessageRecorder {
job =
scope.launch {
while (recorder != null) {
amplitudes.add(((recorder?.maxAmplitude ?: 0) / 100).toInt())
amplitudes.add(recorder?.maxAmplitude?.toFloat() ?: 0f)
delay(1000)
}
}
@@ -20,6 +20,8 @@
*/
package com.vitorpamplona.amethyst.ui.components
import android.R.attr.maxHeight
import android.R.attr.minHeight
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
@@ -48,6 +50,7 @@ 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 com.vitorpamplona.amethyst.ui.components.toDrawableAmplitudes
import kotlin.math.ceil
import kotlin.math.roundToInt
@@ -77,7 +80,7 @@ fun AudioWaveformReadOnly(
spikeRadius: Dp = 2.dp,
spikePadding: Dp = 2.dp,
progress: Float = 0F,
amplitudes: List<Int>,
amplitudes: List<Float>,
onProgressChange: (Float) -> Unit,
) {
val backgroundColor = MaterialTheme.colorScheme.background
@@ -101,6 +104,7 @@ fun AudioWaveformReadOnly(
maxHeight = canvasSize.height.coerceAtLeast(MIN_SPIKE_HEIGHT),
)
}.map { animateFloatAsState(it, spikeAnimationSpec).value }
Canvas(
modifier =
Modifier
@@ -145,14 +149,13 @@ fun AudioWaveformReadOnly(
}
}
private fun List<Int>.toDrawableAmplitudes(
private fun List<Float>.toDrawableAmplitudes(
amplitudeType: AmplitudeType,
spikes: Int,
minHeight: Float,
maxHeight: Float,
): List<Float> {
val amplitudes = map(Int::toFloat)
if (amplitudes.isEmpty() || spikes == 0) {
if (this.isEmpty() || spikes == 0) {
return List(spikes) { minHeight }
}
val transform = { data: List<Float> ->
@@ -161,11 +164,10 @@ private fun List<Int>.toDrawableAmplitudes(
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)
spikes > this.count() -> this.fillToSize(spikes, transform)
else -> this.chunkToSize(spikes, transform)
}.normalize(minHeight, maxHeight)
}
@@ -104,7 +104,7 @@ fun UrlPreviewCard(
AsyncImage(
model = previewInfo.imageUrlFullPath,
contentDescription = previewInfo.title,
contentScale = ContentScale.Crop,
contentScale = ContentScale.FillWidth,
modifier = previewCardImageModifier,
)
@@ -149,11 +149,32 @@ fun AppNavigation(
composableFromEndArgs<Route.Room> { ChatroomScreen(it.toKey(), it.message, it.replyId, it.draftId, accountViewModel, nav) }
composableFromEndArgs<Route.RoomByAuthor> { ChatroomByAuthorScreen(it.id, null, accountViewModel, nav) }
composableFromEndArgs<Route.PublicChatChannel> { PublicChatChannelScreen(it.id, accountViewModel, nav) }
composableFromEndArgs<Route.LiveActivityChannel> { LiveActivityChannelScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
composableFromEndArgs<Route.PublicChatChannel> {
PublicChatChannelScreen(
it.id,
it.draftId?.let { hex -> accountViewModel.getNoteIfExists(hex) },
accountViewModel,
nav,
)
}
composableFromEndArgs<Route.LiveActivityChannel> {
LiveActivityChannelScreen(
Address(it.kind, it.pubKeyHex, it.dTag),
draft = it.draftId?.let { hex -> accountViewModel.getNoteIfExists(hex) },
accountViewModel,
nav,
)
}
composableFromEndArgs<Route.EphemeralChat> {
RelayUrlNormalizer.normalizeOrNull(it.relayUrl)?.let { relay ->
EphemeralChatScreen(RoomId(it.id, relay), accountViewModel, nav)
EphemeralChatScreen(
channelId = RoomId(it.id, relay),
draft = it.draftId?.let { hex -> accountViewModel.getNoteIfExists(hex) },
accountViewModel = accountViewModel,
nav = nav,
)
}
}
@@ -180,6 +201,7 @@ fun AppNavigation(
NewPublicMessageScreen(
to = it.toKey(),
reply = it.replyId?.let { hex -> accountViewModel.getNoteIfExists(hex) },
draft = it.draftId?.let { hex -> accountViewModel.getNoteIfExists(hex) },
accountViewModel = accountViewModel,
nav = nav,
)
@@ -221,7 +243,7 @@ fun AppNavigation(
)
}
composableFromBottomArgs<Route.NewPost> {
composableFromBottomArgs<Route.NewShortNote> {
ShortNotePostScreen(
message = it.message,
attachment = it.attachment?.ifBlank { null }?.toUri(),
@@ -262,7 +284,7 @@ private fun NavigateIfIntentRequested(
if (activity.intent.action == Intent.ACTION_SEND) {
// avoids restarting the new Post screen when the intent is for the screen.
// Microsoft's swift key sends Gifs as new actions
if (isBaseRoute<Route.NewPost>(nav.controller)) return
if (isBaseRoute<Route.NewShortNote>(nav.controller)) return
// saves the intent to avoid processing again
var message by remember {
@@ -279,7 +301,7 @@ private fun NavigateIfIntentRequested(
)
}
nav.newStack(Route.NewPost(message = message, attachment = media.toString()))
nav.newStack(Route.NewShortNote(message = message, attachment = media.toString()))
} else {
var newAccount by remember { mutableStateOf<String?>(null) }
@@ -338,13 +360,13 @@ private fun NavigateIfIntentRequested(
if (intent.action == Intent.ACTION_SEND) {
// avoids restarting the new Post screen when the intent is for the screen.
// Microsoft's swift key sends Gifs as new actions
if (!isBaseRoute<Route.NewPost>(nav.controller)) {
if (!isBaseRoute<Route.NewShortNote>(nav.controller)) {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
nav.newStack(Route.NewPost(message = it))
nav.newStack(Route.NewShortNote(message = it))
}
(intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
nav.newStack(Route.NewPost(attachment = it.toString()))
nav.newStack(Route.NewShortNote(attachment = it.toString()))
}
}
} else {
@@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
@@ -40,6 +41,7 @@ import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
@@ -49,6 +51,7 @@ import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefiniti
import com.vitorpamplona.quartz.nip73ExternalIds.location.isGeohashedScoped
import com.vitorpamplona.quartz.nip73ExternalIds.topics.isHashtagScoped
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
fun routeFor(
note: Note,
@@ -78,6 +81,10 @@ fun routeFor(
innerEvent.activityAddress()?.let {
return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
}
} else if (innerEvent is EphemeralChatEvent) {
innerEvent.roomId()?.let {
return Route.EphemeralChat(it.id, it.relayUrl.url)
}
} else if (innerEvent is ChatroomKeyable) {
val room = innerEvent.chatroomKey(loggedIn.userProfile().pubkeyHex)
loggedIn.chatroomList.getOrCreatePrivateChatroom(room)
@@ -206,7 +213,7 @@ fun routeReplyTo(
users = noteEvent.groupKeySet() - account.userProfile().pubkeyHex,
parentId = noteEvent.id,
)
is TextNoteEvent -> Route.NewPost(baseReplyTo = note.idHex)
is TextNoteEvent -> Route.NewShortNote(baseReplyTo = note.idHex)
is PrivateDmEvent ->
routeToMessage(
room = noteEvent.chatroomKey(account.userProfile().pubkeyHex),
@@ -236,3 +243,49 @@ fun routeReplyTo(
else -> Route.GenericCommentPost(replyTo = note.idHex)
}
}
suspend fun routeEditDraftTo(
note: Note,
account: Account,
): Route? {
val noteEvent = note.event as DraftWrapEvent
val draft = account.draftsDecryptionCache.cachedDraft(noteEvent)
return when (draft) {
is ChannelMessageEvent -> draft.channelId()?.let { Route.PublicChatChannel(it, draftId = note.idHex) }
is LiveActivitiesChatMessageEvent -> {
draft.activityAddress()?.let { Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag, draftId = note.idHex) }
}
is EphemeralChatEvent -> {
draft.roomId()?.let { Route.EphemeralChat(it.id, it.relayUrl.url, draftId = note.idHex) }
}
is ChatroomKeyable -> {
val room = draft.chatroomKey(account.userProfile().pubkeyHex)
account.chatroomList.getOrCreatePrivateChatroom(room)
return Route.Room(room, draftId = note.idHex)
}
is TextNoteEvent -> Route.NewShortNote(draft = note.idHex)
is ClassifiedsEvent -> Route.NewProduct(draft = note.idHex)
is PublicMessageEvent ->
Route.NewPublicMessage(
users = draft.groupKeySet() - account.userProfile().pubkeyHex,
parentId = noteEvent.id,
draftId = note.idHex,
)
is CommentEvent -> {
if (draft.isGeohashedScoped()) {
Route.GeoPost(draft = note.idHex)
} else if (draft.isHashtagScoped()) {
Route.HashtagPost(draft = note.idHex)
} else {
Route.GenericCommentPost(draft = note.idHex)
}
}
else -> null
}
}
@@ -92,16 +92,14 @@ sealed class Route {
@Serializable data class PublicChatChannel(
val id: String,
val draftId: HexKey? = null,
) : Route()
@Serializable data class LiveActivityChannel(
val kind: Int,
val pubKeyHex: HexKey,
val dTag: String,
) : Route()
@Serializable data class EphemeralChatChannel(
val id: String,
val draftId: HexKey? = null,
) : Route()
@Serializable data class RelayInfo(
@@ -111,6 +109,7 @@ sealed class Route {
@Serializable data class EphemeralChat(
val id: String,
val relayUrl: String,
val draftId: HexKey? = null,
) : Route()
@Serializable object NewEphemeralChat : Route()
@@ -143,10 +142,12 @@ sealed class Route {
@Serializable data class NewPublicMessage(
val to: String,
val replyId: HexKey? = null,
val draftId: HexKey? = null,
) : Route() {
constructor(users: Set<HexKey>, parentId: HexKey) : this(
constructor(users: Set<HexKey>, parentId: HexKey, draftId: HexKey? = null) : this(
to = users.joinToString(","),
replyId = parentId,
draftId = draftId,
)
fun toKey(): Set<HexKey> = to.split(",").toSet()
@@ -198,7 +199,7 @@ sealed class Route {
) : Route()
@Serializable
data class NewPost(
data class NewShortNote(
val message: String? = null,
val attachment: String? = null,
val baseReplyTo: String? = null,
@@ -250,7 +251,7 @@ fun getRouteWithArguments(navController: NavHostController): Route? {
dest.hasRoute<Route.EditMediaServers>() -> entry.toRoute<Route.EditMediaServers>()
dest.hasRoute<Route.Nip47NWCSetup>() -> entry.toRoute<Route.Nip47NWCSetup>()
dest.hasRoute<Route.Room>() -> entry.toRoute<Route.Room>()
dest.hasRoute<Route.NewPost>() -> entry.toRoute<Route.NewPost>()
dest.hasRoute<Route.NewShortNote>() -> entry.toRoute<Route.NewShortNote>()
dest.hasRoute<Route.NewProduct>() -> entry.toRoute<Route.NewProduct>()
dest.hasRoute<Route.GeoPost>() -> entry.toRoute<Route.GeoPost>()
dest.hasRoute<Route.HashtagPost>() -> entry.toRoute<Route.HashtagPost>()
@@ -208,6 +208,8 @@ import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
import com.vitorpamplona.quartz.nip71Video.VideoShortEvent
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
@@ -793,6 +795,8 @@ private fun RenderNoteRow(
is FileHeaderEvent -> FileHeaderDisplay(baseNote, true, ContentScale.FillWidth, accountViewModel)
is VideoHorizontalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, ContentScale.FillWidth, accountViewModel, nav)
is VideoVerticalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, ContentScale.FillWidth, accountViewModel, nav)
is VideoNormalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, ContentScale.FillWidth, accountViewModel, nav)
is VideoShortEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, ContentScale.FillWidth, accountViewModel, nav)
is PictureEvent -> PictureDisplay(baseNote, true, ContentScale.FillWidth, PaddingValues(vertical = 5.dp), backgroundColor, accountViewModel, nav)
is BaseVoiceEvent -> RenderVoiceTrack(baseNote, ContentScale.FillWidth, accountViewModel, nav)
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, true, ContentScale.FillWidth, accountViewModel)
@@ -84,7 +84,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.report.ReportNoteDialog
@@ -169,11 +169,9 @@ fun NoteQuickActionMenu(
note = note,
onDismiss = onDismiss,
onWantsToEditDraft = {
nav.nav(
Route.NewPost(
draft = note.idHex,
),
)
nav.nav {
routeEditDraftTo(note, accountViewModel.account)
}
},
accountViewModel = accountViewModel,
nav = nav,
@@ -48,7 +48,6 @@ import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CardDefaults
@@ -155,7 +154,6 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.reactionBox
import com.vitorpamplona.amethyst.ui.theme.ripple24dp
import com.vitorpamplona.amethyst.ui.theme.selectedReactionBoxModifier
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientListener.onError
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
@@ -541,7 +539,7 @@ private fun BoostWithDialog(
accountViewModel,
onQuotePress = {
nav.nav {
Route.NewPost(
Route.NewShortNote(
quote = baseNote.idHex,
version =
(editState.value as? GenericLoadable.Loaded)
@@ -562,7 +560,7 @@ private fun BoostWithDialog(
null
}
Route.NewPost(
Route.NewShortNote(
baseReplyTo = replyTo?.idHex,
fork = baseNote.idHex,
version =
@@ -47,7 +47,7 @@ import com.vitorpamplona.amethyst.ui.actions.EditPostView
import com.vitorpamplona.amethyst.ui.components.ClickableBox
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
import com.vitorpamplona.amethyst.ui.note.externalLinkForNote
import com.vitorpamplona.amethyst.ui.note.types.EditState
@@ -240,11 +240,9 @@ fun NoteDropDownMenu(
DropdownMenuItem(
text = { Text(stringRes(R.string.edit_draft)) },
onClick = {
val route =
Route.NewPost(
draft = note.idHex,
)
nav.nav(route)
nav.nav {
routeEditDraftTo(note, accountViewModel.account)
}
},
)
}
@@ -104,7 +104,7 @@ fun ReplyCommentPostScreen(
val context = LocalContext.current
LaunchedEffect(Unit) {
LaunchedEffect(postViewModel, accountViewModel) {
reply?.let {
postViewModel.reply(it)
}
@@ -1691,7 +1691,7 @@ class AccountViewModel(
class CachedDraftNotes(
val accountViewModel: AccountViewModel,
) : GenericBaseCacheAsync<DraftWrapEvent, Note>(20) {
) : GenericBaseCacheAsync<DraftWrapEvent, Note>(100) {
override suspend fun compute(key: DraftWrapEvent): Note? =
withContext(Dispatchers.IO) {
val decrypted = accountViewModel.account.draftsDecryptionCache.cachedDraft(key)
@@ -199,7 +199,7 @@ class DraftEventHandler(
eventNote: Note,
publicNote: Note,
) {
if (event.pubKey == account.signer.pubKey && !event.isDeleted()) {
if (event.pubKey == account.signer.pubKey && !event.isDeleted() && !LocalCache.deletionIndex.hasBeenDeleted(event)) {
val rumor = account.draftsDecryptionCache.preCachedDraft(event) ?: account.draftsDecryptionCache.cachedDraft(event)
rumor?.let { indexDraftAsRealEvent(eventNote, it) }
}
@@ -138,7 +138,6 @@ fun ObserveImageLoadingTor(accountViewModel: AccountViewModel) {
fun ManageRelayServices(accountViewModel: AccountViewModel) {
val relayServices by Amethyst.instance.relayProxyClientConnector.relayServices
.collectAsStateWithLifecycle()
Log.d("ManageRelayServices", "Relay Services changed $relayServices")
}
@Composable
@@ -21,7 +21,9 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
@@ -49,7 +51,7 @@ fun ChatroomScreen(
},
accountViewModel = accountViewModel,
) {
Column(Modifier.padding(it)) {
Column(Modifier.padding(it).consumeWindowInsets(it).statusBarsPadding()) {
ChatroomView(
room = roomId,
draftMessage = draftMessage,
@@ -70,7 +70,7 @@ fun ChatroomView(
newPostModel.load(room)
if (replyToNote != null) {
LaunchedEffect(key1 = replyToNote) {
LaunchedEffect(key1 = replyToNote, newPostModel, accountViewModel) {
accountViewModel.checkGetOrCreateNote(replyToNote) {
if (it != null) {
newPostModel.reply(it)
@@ -79,7 +79,7 @@ fun ChatroomView(
}
}
if (editFromDraft != null) {
LaunchedEffect(key1 = replyToNote) {
LaunchedEffect(replyToNote, newPostModel, accountViewModel) {
accountViewModel.checkGetOrCreateNote(editFromDraft) {
if (it != null) {
newPostModel.editFromDraft(it)
@@ -151,12 +151,12 @@ fun NewGroupDMScreen(
val context = LocalContext.current
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
message?.ifBlank { null }?.let {
postViewModel.updateMessage(TextFieldValue(it))
}
attachment?.let {
LaunchedEffect(postViewModel, accountViewModel) {
message?.ifBlank { null }?.let {
postViewModel.updateMessage(TextFieldValue(it))
}
attachment?.let {
withContext(Dispatchers.IO) {
val mediaType = context.contentResolver.getType(it)
postViewModel.pickedMedia(persistentListOf(SelectedMedia(it, mediaType)))
}
@@ -25,10 +25,12 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -44,6 +46,7 @@ import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
@Composable
fun EphemeralChatChannelView(
channelId: RoomId?,
draft: Note? = null,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -52,6 +55,7 @@ fun EphemeralChatChannelView(
LoadEphemeralChatChannel(channelId, accountViewModel) { ephem ->
PrepareChannelViewModels(
baseChannel = ephem,
draft = draft,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -61,6 +65,7 @@ fun EphemeralChatChannelView(
@Composable
private fun PrepareChannelViewModels(
baseChannel: EphemeralChatChannel,
draft: Note? = null,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -78,6 +83,12 @@ private fun PrepareChannelViewModels(
channelScreenModel.init(accountViewModel)
channelScreenModel.load(baseChannel)
if (draft != null) {
LaunchedEffect(draft, channelScreenModel, accountViewModel) {
channelScreenModel.editFromDraft(draft)
}
}
ChannelView(
channel = baseChannel,
feedViewModel = feedViewModel,
@@ -22,8 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephem
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -33,6 +35,7 @@ import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
@Composable
fun EphemeralChatScreen(
channelId: RoomId,
draft: Note? = null,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -45,8 +48,8 @@ fun EphemeralChatScreen(
},
accountViewModel = accountViewModel,
) {
Column(Modifier.padding(it)) {
EphemeralChatChannelView(channelId, accountViewModel, nav)
Column(Modifier.padding(it).statusBarsPadding()) {
EphemeralChatChannelView(channelId, draft, accountViewModel, nav)
}
}
}
@@ -25,10 +25,12 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -44,6 +46,7 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
@Composable
fun PublicChatChannelView(
channelId: String?,
draft: Note? = null,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -52,6 +55,7 @@ fun PublicChatChannelView(
LoadPublicChatChannel(channelId, accountViewModel) {
PrepareChannelViewModels(
baseChannel = it,
draft = draft,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -61,6 +65,7 @@ fun PublicChatChannelView(
@Composable
fun PrepareChannelViewModels(
baseChannel: PublicChatChannel,
draft: Note? = null,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -78,6 +83,12 @@ fun PrepareChannelViewModels(
channelScreenModel.init(accountViewModel)
channelScreenModel.load(baseChannel)
if (draft != null) {
LaunchedEffect(draft, channelScreenModel, accountViewModel) {
channelScreenModel.editFromDraft(draft)
}
}
ChannelView(
channel = baseChannel,
feedViewModel = feedViewModel,
@@ -22,8 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel
@@ -34,6 +36,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
@Composable
fun PublicChatChannelScreen(
channelId: HexKey?,
draft: Note?,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -48,8 +51,8 @@ fun PublicChatChannelScreen(
},
accountViewModel = accountViewModel,
) {
Column(Modifier.padding(it)) {
PublicChatChannelView(channelId, accountViewModel, nav)
Column(Modifier.padding(it).statusBarsPadding()) {
PublicChatChannelView(channelId, draft, accountViewModel, nav)
}
}
}
@@ -25,10 +25,12 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -45,6 +47,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
@Composable
fun LiveActivityChannelView(
channelId: Address?,
draft: Note? = null,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -53,6 +56,7 @@ fun LiveActivityChannelView(
LoadLiveActivityChannel(channelId, accountViewModel) {
PrepareChannelViewModels(
baseChannel = it,
draft = draft,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -62,6 +66,7 @@ fun LiveActivityChannelView(
@Composable
fun PrepareChannelViewModels(
baseChannel: LiveActivitiesChannel,
draft: Note? = null,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -79,6 +84,12 @@ fun PrepareChannelViewModels(
channelScreenModel.init(accountViewModel)
channelScreenModel.load(baseChannel)
if (draft != null) {
LaunchedEffect(draft, channelScreenModel, accountViewModel) {
channelScreenModel.editFromDraft(draft)
}
}
LiveActivityChannelView(
channel = baseChannel,
feedViewModel = feedViewModel,
@@ -22,8 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel
@@ -34,6 +36,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
@Composable
fun LiveActivityChannelScreen(
channelId: Address?,
draft: Note? = null,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -48,8 +51,8 @@ fun LiveActivityChannelScreen(
},
accountViewModel = accountViewModel,
) {
Column(Modifier.padding(it)) {
LiveActivityChannelView(channelId, accountViewModel, nav)
Column(Modifier.padding(it).statusBarsPadding()) {
LiveActivityChannelView(channelId, draft, accountViewModel, nav)
}
}
}
@@ -207,10 +207,10 @@ open class ChannelNewMessageViewModel :
private fun loadFromDraft(draft: Note) {
val draftEvent = draft.event ?: return
val localfowardZapTo = draftEvent.tags.zapSplitSetup()
val totalWeight = localfowardZapTo.sumOf { it.weight }
val localForwardZapTo = draftEvent.tags.zapSplitSetup()
val totalWeight = localForwardZapTo.sumOf { it.weight }
forwardZapTo = SplitBuilder()
localfowardZapTo.forEach {
localForwardZapTo.forEach {
if (it is ZapSplitSetup) {
val user = LocalCache.getOrCreateUser(it.pubKeyHex)
forwardZapTo.addItem(user, (it.weight / totalWeight).toFloat())
@@ -218,7 +218,7 @@ open class ChannelNewMessageViewModel :
// don't support edditing old-style splits.
}
forwardZapToEditting = TextFieldValue("")
wantsForwardZapTo = localfowardZapTo.isNotEmpty()
wantsForwardZapTo = localForwardZapTo.isNotEmpty()
wantsToMarkAsSensitive = draftEvent.isSensitive()
@@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.utils.mapOfSet
fun filterFollowingPublicChats(
fun filterFollowingPublicChatsCreationEvent(
followingChannels: Set<HexKey>,
since: SincePerRelayMap?,
): List<RelayBasedFilter>? {
@@ -38,13 +38,16 @@ fun filterFollowingPublicChats(
val relayRoomDTags =
mapOfSet {
followingChannels.forEach { channelId ->
val relays =
LocalCache.getPublicChatChannelIfExists(channelId)?.relays()
?: LocalCache.relayHints.hintsForEvent(channelId).ifEmpty { null }
?: Constants.eventFinderRelays
val channel = LocalCache.getPublicChatChannelIfExists(channelId)
if (channel?.event == null) {
val relays =
channel?.relays()?.ifEmpty { null }
?: LocalCache.relayHints.hintsForEvent(channelId).ifEmpty { null }
?: Constants.eventFinderRelays
relays.forEach { relayUrl ->
add(relayUrl, channelId)
relays.forEach { relayUrl ->
add(relayUrl, channelId)
}
}
}
}
@@ -40,7 +40,7 @@ fun filterLastMessageFollowingPublicChats(
mapOfSet {
followingChannels.forEach { channelId ->
val relays =
LocalCache.getPublicChatChannelIfExists(channelId)?.relays()
LocalCache.getPublicChatChannelIfExists(channelId)?.relays()?.ifEmpty { null }
?: LocalCache.relayHints.hintsForEvent(channelId).ifEmpty { null }
?: Constants.eventFinderRelays
@@ -43,7 +43,7 @@ class FollowingPublicChatSubAssembler(
): List<RelayBasedFilter>? =
listOfNotNull(
filterLastMessageFollowingPublicChats(key.account.publicChatList.flowSet.value, since),
filterFollowingPublicChats(key.account.publicChatList.flowSet.value, since),
filterFollowingPublicChatsCreationEvent(key.account.publicChatList.flowSet.value, since),
).flatten()
override fun user(key: ChatroomListState) = key.account.userProfile()
@@ -57,7 +57,7 @@ fun NewCommunityNoteButton(
FloatingActionButton(
onClick = {
val route =
Route.NewPost(
Route.NewShortNote(
baseReplyTo = note.idHex,
)
nav.nav(route)
@@ -101,7 +101,7 @@ fun NewProductScreen(
val context = LocalContext.current
LaunchedEffect(Unit) {
LaunchedEffect(postViewModel, accountViewModel) {
postViewModel.reloadRelaySet()
draft?.let {
postViewModel.editFromDraft(it)
@@ -53,7 +53,7 @@ fun GeoHashPostScreen(
val context = LocalContext.current
LaunchedEffect(Unit) {
LaunchedEffect(postViewModel, accountViewModel) {
geohash?.let {
postViewModel.newPostFor(GeohashId(it))
}
@@ -53,7 +53,7 @@ fun HashtagPostScreen(
val context = LocalContext.current
LaunchedEffect(Unit) {
LaunchedEffect(postViewModel, accountViewModel) {
hashtag?.let {
postViewModel.newPostFor(HashtagId(it))
}
@@ -38,7 +38,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
fun NewNoteButton(nav: INav) {
FloatingActionButton(
onClick = {
nav.nav(Route.NewPost())
nav.nav(Route.NewShortNote())
},
modifier = Size55Modifier,
shape = CircleShape,
@@ -99,7 +99,7 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
@Composable
@@ -120,13 +120,13 @@ fun ShortNotePostScreen(
val context = LocalContext.current
val activity = context.getActivity()
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
postViewModel.load(baseReplyTo, quote, fork, version, draft)
message?.ifBlank { null }?.let {
postViewModel.updateMessage(TextFieldValue(it))
}
attachment?.let {
LaunchedEffect(postViewModel, accountViewModel) {
postViewModel.load(baseReplyTo, quote, fork, version, draft)
message?.ifBlank { null }?.let {
postViewModel.updateMessage(TextFieldValue(it))
}
attachment?.let {
withContext(Dispatchers.IO) {
val mediaType = context.contentResolver.getType(it)
postViewModel.selectImage(persistentListOf(SelectedMedia(it, mediaType)))
}
@@ -86,7 +86,9 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris
import com.vitorpamplona.quartz.nip10Notes.content.findURLs
import com.vitorpamplona.quartz.nip10Notes.tags.markedETags
import com.vitorpamplona.quartz.nip10Notes.tags.notify
import com.vitorpamplona.quartz.nip10Notes.tags.prepareETagsAsReplyTo
import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes
import com.vitorpamplona.quartz.nip18Reposts.quotes.taggedQuoteIds
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
@@ -544,12 +546,47 @@ open class ShortNotePostViewModel :
imetas(usedAttachments)
}
} else {
TextNoteEvent.build(
note = tagger.message,
replyingTo = originalNote?.toEventHint<TextNoteEvent>(),
forkingFrom = forkedFromNote?.toEventHint<TextNoteEvent>(),
) {
tagger.pTags?.let { pTagList -> notify(pTagList.map { it.toPTag() }) }
TextNoteEvent.build(tagger.message) {
val replyingTo = originalNote?.toEventHint<TextNoteEvent>()
val forkingFrom = forkedFromNote?.toEventHint<TextNoteEvent>()
if (replyingTo != null || forkingFrom != null) {
val tags = prepareETagsAsReplyTo(replyingTo, forkingFrom)
// fixes wrong tags from previous clients
tags.forEach {
val note = accountViewModel.getNoteIfExists(it.eventId)
val ourAuthor = note?.author?.pubkeyHex
val ourHint = note?.relayHintUrl()
if (it.author == null || it.author?.isBlank() == true) {
it.author = ourAuthor
} else {
if (ourAuthor != null && it.author != ourAuthor) {
it.author = ourAuthor
}
}
if (it.relay == null) {
it.relay = ourHint
} else {
if (ourHint != null && it.relay != ourHint) {
it.relay = ourHint
}
}
}
markedETags(tags)
}
tagger.pTags?.let { userList ->
val tags =
userList.map {
val tag = it.toPTag()
if (tag.relayHint == null) {
tag.copy(relayHint = LocalCache.relayHints.hintsForKey(it.pubkeyHex).firstOrNull())
} else {
tag
}
}
notify(tags)
}
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
@@ -105,13 +105,14 @@ import kotlinx.coroutines.withContext
fun NewPublicMessageScreen(
to: Set<HexKey>? = null,
reply: Note? = null,
draft: Note? = null,
accountViewModel: AccountViewModel,
nav: Nav,
) {
val postViewModel: NewPublicMessageViewModel = viewModel()
postViewModel.init(accountViewModel)
LaunchedEffect(Unit) {
LaunchedEffect(postViewModel, accountViewModel) {
withContext(Dispatchers.IO) {
to?.let {
postViewModel.load(it)
@@ -119,6 +120,9 @@ fun NewPublicMessageScreen(
reply?.let {
postViewModel.reply(it)
}
draft?.let {
postViewModel.editFromDraft(it)
}
}
}
@@ -28,6 +28,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
import com.vitorpamplona.quartz.nip71Video.VideoShortEvent
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
val UserProfileMediaKinds =
@@ -36,6 +38,8 @@ val UserProfileMediaKinds =
ProfileGalleryEntryEvent.KIND,
VideoVerticalEvent.KIND,
VideoHorizontalEvent.KIND,
VideoNormalEvent.KIND,
VideoShortEvent.KIND,
)
fun filterUserProfileMedia(
@@ -59,7 +59,7 @@ fun AddDMRelayListDialog(
postViewModel.init(accountViewModel)
LaunchedEffect(postViewModel, accountViewModel.account) {
LaunchedEffect(postViewModel, accountViewModel) {
postViewModel.load()
}
@@ -431,7 +431,7 @@ fun ReactionsColumn(
iconSize = Size40dp,
onQuotePress = {
nav.nav(
Route.NewPost(
Route.NewShortNote(
quote = baseNote.idHex,
),
)
@@ -37,6 +37,8 @@ import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip68Picture.PictureMeta
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoMeta
import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
import com.vitorpamplona.quartz.nip71Video.VideoShortEvent
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
@@ -97,6 +99,10 @@ class VideoFeedFilter(
fun acceptanceEvent(noteEvent: VideoHorizontalEvent) = acceptableVideoiMetas(noteEvent.imetaTags())
fun acceptanceEvent(noteEvent: VideoNormalEvent) = acceptableVideoiMetas(noteEvent.imetaTags())
fun acceptanceEvent(noteEvent: VideoShortEvent) = acceptableVideoiMetas(noteEvent.imetaTags())
fun acceptableEvent(
note: Note,
params: FilterByListParams,
@@ -111,6 +117,8 @@ class VideoFeedFilter(
(noteEvent is FileHeaderEvent && acceptanceEvent(noteEvent)) ||
(noteEvent is VideoVerticalEvent && acceptanceEvent(noteEvent)) ||
(noteEvent is VideoHorizontalEvent && acceptanceEvent(noteEvent)) ||
(noteEvent is VideoNormalEvent && acceptanceEvent(noteEvent)) ||
(noteEvent is VideoShortEvent && acceptanceEvent(noteEvent)) ||
(noteEvent is FileStorageHeaderEvent && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET)) ||
(noteEvent is PictureEvent && acceptanceEvent(noteEvent))
) &&
@@ -23,15 +23,31 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
import com.vitorpamplona.quartz.nip71Video.VideoShortEvent
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
val SUPPORTED_VIDEO_FEED_MIME_TYPES = listOf("image/jpeg", "image/gif", "image/png", "image/webp", "video/mp4", "video/mpeg", "video/webm", "audio/aac", "audio/mpeg", "audio/webm", "audio/wav", "image/avif")
val SUPPORTED_VIDEO_FEED_MIME_TYPES_SET = SUPPORTED_VIDEO_FEED_MIME_TYPES.toSet()
val PictureAndVideoKinds = listOf(PictureEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND)
val PictureAndVideoKTags = listOf(PictureEvent.KIND.toString(), VideoHorizontalEvent.KIND.toString(), VideoVerticalEvent.KIND.toString())
val PictureAndVideoKinds =
listOf(
PictureEvent.KIND,
VideoHorizontalEvent.KIND,
VideoVerticalEvent.KIND,
VideoNormalEvent.KIND,
VideoShortEvent.KIND,
)
val PictureAndVideoKTags =
listOf(
PictureEvent.KIND.toString(),
VideoHorizontalEvent.KIND.toString(),
VideoVerticalEvent.KIND.toString(),
VideoNormalEvent.KIND.toString(),
VideoShortEvent.KIND.toString(),
)
val PictureAndVideoLegacyKinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND)
val PictureAndVideoLegacyKTags = listOf(FileHeaderEvent.KIND.toString(), FileStorageHeaderEvent.KIND.toString())
val LegacyMimeTypes = SUPPORTED_VIDEO_FEED_MIME_TYPES
@@ -112,7 +112,7 @@ fun LoginPage(
val loginViewModel: LoginViewModel = viewModel()
loginViewModel.init(accountStateViewModel)
LaunchedEffect(Unit) {
LaunchedEffect(loginViewModel, isFirstLogin, newAccountKey) {
loginViewModel.load(isFirstLogin, newAccountKey)
}
@@ -62,7 +62,7 @@ class TorService(
delay(100)
}
trySend(TorServiceStatus.Active(torService.socksPort))
trySend(TorServiceStatus.Active(torService.socksPort, torService.torControlConnection))
Log.d("TorService", "Tor Service Connected ${torService.socksPort}")
}
}
@@ -20,9 +20,12 @@
*/
package com.vitorpamplona.amethyst.ui.tor
import net.freehaven.tor.control.TorControlConnection
sealed class TorServiceStatus {
data class Active(
val port: Int,
val torControlConnection: TorControlConnection,
) : TorServiceStatus()
object Off : TorServiceStatus()
+4 -4
View File
@@ -1,7 +1,7 @@
[versions]
accompanistAdaptive = "0.37.3"
activityCompose = "1.10.1"
agp = "8.12.0"
agp = "8.12.1"
android-compileSdk = "36"
android-minSdk = "26"
android-targetSdk = "36"
@@ -24,10 +24,10 @@ jacksonModuleKotlin = "2.19.2"
jna = "5.17.0"
jtorctl = "0.4.5.7"
junit = "4.13.2"
kotlin = "2.2.0"
kotlin = "2.2.10"
kotlinxCollectionsImmutable = "0.4.0"
kotlinxSerialization = "1.9.0"
kotlinxSerializationPlugin = "2.2.0"
kotlinxSerializationPlugin = "2.2.10"
languageId = "17.0.6"
lazysodiumAndroid = "5.2.0"
lifecycleRuntimeKtx = "2.9.2"
@@ -43,7 +43,7 @@ rfc3986 = "0.1.2"
secp256k1KmpJniAndroid = "0.18.0"
securityCryptoKtx = "1.1.0"
spotless = "7.2.1"
torAndroid = "0.4.8.17.1"
torAndroid = "0.4.8.17.2"
translate = "17.0.3"
unifiedpush = "3.0.10"
urlDetector = "0.1.23"
@@ -108,6 +108,8 @@ import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
import com.vitorpamplona.quartz.nip71Video.VideoShortEvent
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
@@ -286,6 +288,8 @@ class EventFactory {
TrustedRelayListEvent.KIND -> TrustedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
VideoHorizontalEvent.KIND -> VideoHorizontalEvent(id, pubKey, createdAt, tags, content, sig)
VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig)
VideoNormalEvent.KIND -> VideoNormalEvent(id, pubKey, createdAt, tags, content, sig)
VideoShortEvent.KIND -> VideoShortEvent(id, pubKey, createdAt, tags, content, sig)
VoiceEvent.KIND -> VoiceEvent(id, pubKey, createdAt, tags, content, sig)
VoiceReplyEvent.KIND -> VoiceReplyEvent(id, pubKey, createdAt, tags, content, sig)
WikiNoteEvent.KIND -> WikiNoteEvent(id, pubKey, createdAt, tags, content, sig)
@@ -54,7 +54,7 @@ class AudioHeaderEvent(
description: String,
downloadUrl: String,
streamUrl: String? = null,
wavefront: List<Int>? = null,
wavefront: List<Float>? = null,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<AudioHeaderEvent>.() -> Unit = {},
) = eventTemplate(KIND, description, createdAt) {
@@ -29,4 +29,4 @@ fun TagArrayBuilder<AudioHeaderEvent>.downloadUrl(downloadUrlTag: String) = addU
fun TagArrayBuilder<AudioHeaderEvent>.streamUrl(streamUrl: String) = addUnique(StreamUrlTag.assemble(streamUrl))
fun TagArrayBuilder<AudioHeaderEvent>.wavefront(wave: List<Int>) = addUnique(WaveformTag.assemble(wave))
fun TagArrayBuilder<AudioHeaderEvent>.wavefront(wave: List<Float>) = addUnique(WaveformTag.assemble(wave))
@@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
import com.vitorpamplona.quartz.utils.ensure
class WaveformTag(
val wave: List<Int>,
val wave: List<Float>,
) {
fun toTagArray() = assemble(wave)
@@ -38,12 +38,12 @@ class WaveformTag(
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
val wave = runCatching { JsonMapper.mapper.readValue<List<Int>>(tag[1]) }.getOrNull()
val wave = runCatching { JsonMapper.mapper.readValue<List<Float>>(tag[1]) }.getOrNull()
if (wave.isNullOrEmpty()) return null
return WaveformTag(wave)
}
@JvmStatic
fun assemble(wave: List<Int>) = arrayOf(TAG_NAME, JsonMapper.mapper.writeValueAsString(wave))
fun assemble(wave: List<Float>) = arrayOf(TAG_NAME, JsonMapper.mapper.writeValueAsString(wave))
}
}
@@ -128,7 +128,7 @@ class EphemeralChatListEvent(
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip04(privateTags, signer),
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = tags,
signer = signer,
createdAt = createdAt,
@@ -168,7 +168,7 @@ class EphemeralChatListEvent(
initializer: TagArrayBuilder<EphemeralChatListEvent>.() -> Unit = {},
) = eventTemplate<EphemeralChatListEvent>(
kind = KIND,
description = PrivateTagsInContent.encryptNip04(privateRooms.map { it.toTagArray() }.toTypedArray(), signer),
description = PrivateTagsInContent.encryptNip44(privateRooms.map { it.toTagArray() }.toTypedArray(), signer),
createdAt = createdAt,
) {
alt(ALT)
@@ -133,6 +133,8 @@ class NostrClient(
relayPool.disconnect()
}
fun isActive() = isActive
@Synchronized
fun reconnect(onlyIfChanged: Boolean = false) {
if (onlyIfChanged) {
@@ -75,23 +75,27 @@ class RelayPool(
fun reconnectIfNeedsToORIfItIsTime() {
if (lastReconnectCall < TimeUtils.oneMinuteAgo()) {
relays.forEach { url, relay ->
if (relay.isConnected()) {
if (relay.needsToReconnect()) {
// network has changed, force reconnect
relay.disconnect()
relay.connect()
}
} else {
// relay is not connected. Connect if it is time
relay.connectAndSyncFiltersIfDisconnected()
}
}
reconnectIfNeedsTo()
lastReconnectCall = TimeUtils.now()
}
}
fun reconnectIfNeedsTo() {
relays.forEach { url, relay ->
if (relay.isConnected()) {
if (relay.needsToReconnect()) {
// network has changed, force reconnect
relay.disconnect()
relay.connect()
}
} else {
// relay is not connected. Connect if it is time
relay.connectAndSyncFiltersIfDisconnected()
}
}
}
fun connect() =
relays.forEach { url, relay ->
relay.connect()
@@ -30,7 +30,7 @@ import okhttp3.Response
class BasicOkHttpWebSocket(
val url: NormalizedRelayUrl,
val httpClient: OkHttpClient,
val httpClient: (NormalizedRelayUrl) -> OkHttpClient,
val out: WebSocketListener,
) : WebSocket {
private var socket: okhttp3.WebSocket? = null
@@ -74,7 +74,7 @@ class BasicOkHttpWebSocket(
) = out.onFailure(t, r?.code, r?.message)
}
socket = httpClient.newWebSocket(request, listener)
socket = httpClient(url).newWebSocket(request, listener)
}
override fun disconnect() {
@@ -85,7 +85,7 @@ class BasicOkHttpWebSocket(
override fun send(msg: String): Boolean = socket?.send(msg) ?: false
class Builder(
val httpClient: OkHttpClient,
val httpClient: (NormalizedRelayUrl) -> OkHttpClient,
) : WebsocketBuilder {
// Called when connecting.
override fun build(
@@ -118,6 +118,15 @@ class TextNoteEvent(
return ALT + msg.take(50) + "..."
}
fun build(
note: String,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<TextNoteEvent>.() -> Unit = {},
) = eventTemplate(KIND, note, createdAt) {
alt(shortedMessageForAlt(note))
initializer()
}
fun build(
note: String,
replyingTo: EventHintBundle<TextNoteEvent>? = null,
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.quartz.nip10Notes.tags
import android.R.attr.tag
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
@@ -27,7 +28,7 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
fun <T : BaseThreadedEvent> TagArrayBuilder<T>.markedETag(tag: MarkedETag) = add(tag.toTagArray())
fun <T : BaseThreadedEvent> TagArrayBuilder<T>.markedETags(tag: List<MarkedETag>) = addAll(tag.map { it.toTagArray() })
fun <T : BaseThreadedEvent> TagArrayBuilder<T>.markedETags(tags: List<MarkedETag>) = addAll(tags.map { it.toTagArray() })
fun TagArrayBuilder<TextNoteEvent>.notify(tag: PTag) = add(tag.toTagArray())
@@ -155,7 +155,7 @@ class ChannelListEvent(
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip04(privateTags, signer),
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = tags,
signer = signer,
createdAt = createdAt,
@@ -206,7 +206,7 @@ class ChannelListEvent(
initializer: TagArrayBuilder<ChannelListEvent>.() -> Unit = {},
) = eventTemplate<ChannelListEvent>(
kind = KIND,
description = PrivateTagsInContent.encryptNip04(privateChannels.map { it.toTagArray() }.toTypedArray(), signer),
description = PrivateTagsInContent.encryptNip44(privateChannels.map { it.toTagArray() }.toTypedArray(), signer),
createdAt = createdAt,
) {
alt(ALT)
@@ -34,6 +34,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip40Expiration.expiration
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
@@ -91,6 +92,11 @@ class DraftWrapEvent(
alt(ALT_DESCRIPTION)
dTag(dTag)
kind(draft.kind)
expiration(TimeUtils.ninetyDaysFromNow())
if (draft is ExposeInDraft) {
addAll(draft.exposeInDraft())
}
initializer()
}
@@ -105,6 +111,7 @@ class DraftWrapEvent(
createdAt = createdAt,
) {
alt(ALT_DESCRIPTION)
expiration(TimeUtils.oneMinuteFromNow())
dTag(dTag)
initializer()
}
@@ -33,7 +33,7 @@ class PrivateTagArrayBuilder {
): Pair<String, Array<Array<String>>> =
if (toPrivate) {
val encryptedTags =
PrivateTagsInContent.encryptNip04(
PrivateTagsInContent.encryptNip44(
privateTags = tags,
signer = signer,
)
@@ -51,7 +51,7 @@ class PrivateTagArrayBuilder {
if (toPrivate) {
val privateTags = current.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
val encryptedTags =
PrivateTagsInContent.encryptNip04(
PrivateTagsInContent.encryptNip44(
privateTags = privateTags.plus(newTag),
signer = signer,
)
@@ -69,7 +69,7 @@ class PrivateTagArrayBuilder {
if (toPrivate) {
val privateTags = current.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
val encryptedTags =
PrivateTagsInContent.encryptNip04(
PrivateTagsInContent.encryptNip44(
privateTags = privateTags.plus(newTag),
signer = signer,
)
@@ -112,7 +112,7 @@ class PrivateTagArrayBuilder {
): Pair<String, Array<Array<String>>> {
val privateTags = current.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
val encryptedTags =
PrivateTagsInContent.encryptNip04(
PrivateTagsInContent.encryptNip44(
privateTags = privateTags.replaceAll(oldTagStartsWith, newTag),
signer = signer,
)
@@ -127,7 +127,7 @@ class PrivateTagArrayBuilder {
): Pair<String, Array<Array<String>>> {
val privateTags = current.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
val encryptedTags =
PrivateTagsInContent.encryptNip04(
PrivateTagsInContent.encryptNip44(
privateTags = privateTags.remove(oldTagStartsWith),
signer = signer,
)
@@ -141,7 +141,7 @@ class PrivateTagArrayBuilder {
): Pair<String, Array<Array<String>>> {
val privateTags = current.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
val encryptedTags =
PrivateTagsInContent.encryptNip04(
PrivateTagsInContent.encryptNip44(
privateTags = privateTags.remove(oldTagStartsWith),
signer = signer,
)
@@ -161,7 +161,7 @@ class PrivateTagArrayBuilder {
): Pair<String, Array<Array<String>>> {
val privateTags = current.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
val encryptedTags =
PrivateTagsInContent.encryptNip04(
PrivateTagsInContent.encryptNip44(
privateTags = privateTags.remove(oldTagStartsWith),
signer = signer,
)
@@ -174,7 +174,7 @@ class PrivateTagArrayBuilder {
signer: NostrSigner,
): Pair<String, Array<Array<String>>> {
val encryptedTags =
PrivateTagsInContent.encryptNip04(
PrivateTagsInContent.encryptNip44(
privateTags = arrayOf(newTag),
signer = signer,
)
@@ -154,7 +154,7 @@ class BookmarkListEvent(
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip04(privateTags, signer),
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = tags,
signer = signer,
createdAt = createdAt,
@@ -198,7 +198,7 @@ class BookmarkListEvent(
initializer: TagArrayBuilder<BookmarkListEvent>.() -> Unit = {},
) = eventTemplate(
kind = KIND,
description = PrivateTagsInContent.encryptNip04(privateBookmarks.map { it.toTagArray() }.toTypedArray(), signer),
description = PrivateTagsInContent.encryptNip44(privateBookmarks.map { it.toTagArray() }.toTypedArray(), signer),
createdAt = createdAt,
) {
dTag(dTag)
@@ -48,15 +48,6 @@ class PrivateTagsInContent {
}
}
suspend fun encryptNip04(
privateTags: Array<Array<String>>? = null,
signer: NostrSigner,
): String =
signer.nip04Encrypt(
if (privateTags.isNullOrEmpty()) "" else encode(privateTags),
signer.pubKey,
)
suspend fun encryptNip44(
privateTags: Array<Array<String>>? = null,
signer: NostrSigner,
@@ -74,15 +65,6 @@ class PrivateTagsInContent {
return decode(json)
}
fun encryptNip04(
privateTags: Array<Array<String>>? = null,
signer: NostrSignerSync,
): String =
signer.nip04Encrypt(
if (privateTags.isNullOrEmpty()) "" else encode(privateTags),
signer.pubKey,
)
fun encryptNip44(
privateTags: Array<Array<String>>? = null,
signer: NostrSignerSync,
@@ -157,7 +157,7 @@ class GeohashListEvent(
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip04(privateTags, signer),
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = tags,
signer = signer,
createdAt = createdAt,
@@ -208,7 +208,7 @@ class GeohashListEvent(
initializer: TagArrayBuilder<GeohashListEvent>.() -> Unit = {},
) = eventTemplate<GeohashListEvent>(
kind = KIND,
description = PrivateTagsInContent.encryptNip04(privateGeohashes.map { GeoHashTag.assembleSingle(it) }.toTypedArray(), signer),
description = PrivateTagsInContent.encryptNip44(privateGeohashes.map { GeoHashTag.assembleSingle(it) }.toTypedArray(), signer),
createdAt = createdAt,
) {
alt(ALT)
@@ -154,7 +154,7 @@ class HashtagListEvent(
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip04(privateTags, signer),
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = tags,
signer = signer,
createdAt = createdAt,
@@ -205,7 +205,7 @@ class HashtagListEvent(
initializer: TagArrayBuilder<HashtagListEvent>.() -> Unit = {},
) = eventTemplate<HashtagListEvent>(
kind = KIND,
description = PrivateTagsInContent.encryptNip04(privateHashtags.map { HashtagTag.assemble(it) }.toTypedArray(), signer),
description = PrivateTagsInContent.encryptNip44(privateHashtags.map { HashtagTag.assemble(it) }.toTypedArray(), signer),
createdAt = createdAt,
) {
alt(ALT)
@@ -139,7 +139,7 @@ class MuteListEvent(
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip04(privateTags, signer),
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = publicTags,
signer = signer,
createdAt = createdAt,
@@ -179,7 +179,7 @@ class MuteListEvent(
initializer: TagArrayBuilder<MuteListEvent>.() -> Unit = {},
) = eventTemplate<MuteListEvent>(
kind = KIND,
description = PrivateTagsInContent.encryptNip04(privateMutes.map { it.toTagArray() }.toTypedArray(), signer),
description = PrivateTagsInContent.encryptNip44(privateMutes.map { it.toTagArray() }.toTypedArray(), signer),
createdAt = createdAt,
) {
alt(ALT)
@@ -161,7 +161,7 @@ class PeopleListEvent(
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip04(privateTags, signer),
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = publicTags,
signer = signer,
createdAt = createdAt,
@@ -205,7 +205,7 @@ class PeopleListEvent(
initializer: TagArrayBuilder<PeopleListEvent>.() -> Unit = {},
) = eventTemplate<PeopleListEvent>(
kind = KIND,
description = PrivateTagsInContent.encryptNip04(privatePeople.map { it.toTagArray() }.toTypedArray(), signer),
description = PrivateTagsInContent.encryptNip44(privatePeople.map { it.toTagArray() }.toTypedArray(), signer),
createdAt = createdAt,
) {
dTag(dTag)
@@ -108,7 +108,7 @@ class BlockedRelayListEvent(
initializer: TagArrayBuilder<BlockedRelayListEvent>.() -> Unit = {},
) = eventTemplate<BlockedRelayListEvent>(
kind = KIND,
description = PrivateTagsInContent.encryptNip04(privateRelays.map { RelayTag.assemble(it) }.toTypedArray(), signer),
description = PrivateTagsInContent.encryptNip44(privateRelays.map { RelayTag.assemble(it) }.toTypedArray(), signer),
createdAt = createdAt,
) {
alt(ALT)
@@ -0,0 +1,80 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip71Video
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
import com.vitorpamplona.quartz.nip22Comments.RootScope
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.utils.TimeUtils
import java.util.UUID
@Immutable
class VideoNormalEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : VideoEvent(id, pubKey, createdAt, KIND, tags, content, sig),
RootScope {
companion object {
const val KIND = 21
const val ALT_DESCRIPTION = "Horizontal Video"
fun build(
video: VideoMeta,
description: String,
dTag: String = UUID.randomUUID().toString(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<VideoNormalEvent>.() -> Unit = {},
) = build(description, dTag, createdAt) {
videoIMeta(video)
initializer()
}
fun build(
video: List<VideoMeta>,
description: String,
dTag: String = UUID.randomUUID().toString(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<VideoNormalEvent>.() -> Unit = {},
) = build(description, dTag, createdAt) {
videoIMetas(video)
initializer()
}
fun build(
description: String,
dTag: String = UUID.randomUUID().toString(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<VideoNormalEvent>.() -> Unit = {},
) = eventTemplate(KIND, description, createdAt) {
dTag(dTag)
alt(ALT_DESCRIPTION)
initializer()
}
}
}
@@ -18,37 +18,52 @@
* 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.quartz.nip37Drafts
package com.vitorpamplona.quartz.nip71Video
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind
import com.vitorpamplona.quartz.nip22Comments.RootScope
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.utils.TimeUtils
import java.util.UUID
class DraftBuilder {
@Immutable
class VideoShortEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : VideoEvent(id, pubKey, createdAt, KIND, tags, content, sig),
RootScope {
companion object {
suspend fun <T : Event> encryptAndSign(
dTag: String,
draft: T,
signer: NostrSigner,
const val KIND = 22
const val ALT_DESCRIPTION = "Vertical Video"
fun build(
video: VideoMeta,
description: String,
dTag: String = UUID.randomUUID().toString(),
createdAt: Long = TimeUtils.now(),
): DraftWrapEvent {
val encryptedContent = signer.nip44Encrypt(draft.toJson(), signer.pubKey)
val template =
eventTemplate<DraftWrapEvent>(DraftWrapEvent.KIND, encryptedContent, createdAt) {
alt(DraftWrapEvent.ALT_DESCRIPTION)
dTag(dTag)
kind(draft.kind)
initializer: TagArrayBuilder<VideoShortEvent>.() -> Unit = {},
) = build(description, dTag, createdAt) {
videoIMeta(video)
initializer()
}
if (draft is ExposeInDraft) {
addAll(draft.exposeInDraft())
}
}
return signer.sign(template)
fun build(
description: String,
dTag: String = UUID.randomUUID().toString(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<VideoShortEvent>.() -> Unit = {},
) = eventTemplate(KIND, description, createdAt) {
dTag(dTag)
alt(ALT_DESCRIPTION)
initializer()
}
}
}
@@ -178,7 +178,7 @@ class CommunityListEvent(
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
) = resign(
content = PrivateTagsInContent.encryptNip04(privateTags, signer),
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
tags = tags,
signer = signer,
createdAt = createdAt,
@@ -218,7 +218,7 @@ class CommunityListEvent(
initializer: TagArrayBuilder<CommunityListEvent>.() -> Unit = {},
) = eventTemplate<CommunityListEvent>(
kind = KIND,
description = PrivateTagsInContent.encryptNip04(privateCommunities.map { it.toTagArray() }.toTypedArray(), signer),
description = PrivateTagsInContent.encryptNip44(privateCommunities.map { it.toTagArray() }.toTypedArray(), signer),
createdAt = createdAt,
) {
alt(ALT)
@@ -29,7 +29,7 @@ data class AudioMeta(
val mimeType: String? = null,
val hash: String? = null,
val duration: Int? = null,
val waveform: List<Int>? = null,
val waveform: List<Float>? = null,
) {
fun toIMetaArray(): Array<String> =
IMetaTagBuilder(url)
@@ -37,7 +37,7 @@ data class AudioMeta(
mimeType?.let { mimeType(it) }
hash?.let { hash(it) }
duration?.let { duration(it) }
waveform?.let { waveform(it) }
waveform?.let { waveformFloat(it) }
}.build()
.toTagArray()
@@ -34,6 +34,8 @@ fun IMetaTagBuilder.hash(hash: HexKey) = add(HashSha256Tag.TAG_NAME, hash)
fun IMetaTagBuilder.duration(size: Int) = add(DurationTag.TAG_NAME, size.toString())
fun IMetaTagBuilder.waveform(wave: List<Int>) = add(WaveformTag.TAG_NAME, WaveformTag.assembleWave(wave))
fun IMetaTagBuilder.waveformInt(wave: List<Int>) = add(WaveformTag.TAG_NAME, WaveformTag.assembleWaveInt(wave))
fun IMetaTagBuilder.waveformFloat(wave: List<Float>) = add(WaveformTag.TAG_NAME, WaveformTag.assembleWaveFloat(wave))
fun IMetaTagBuilder.mimeType(mime: String) = add(MimeTypeTag.TAG_NAME, mime)
@@ -33,7 +33,7 @@ fun <T : BaseVoiceEvent> TagArrayBuilder<T>.audioIMeta(
mimeType: String? = null,
hash: String? = null,
duration: Int? = null,
waveform: List<Int>? = null,
waveform: List<Float>? = null,
) = audioIMeta(AudioMeta(url, mimeType, hash, duration, waveform))
fun <T : BaseVoiceEvent> TagArrayBuilder<T>.audioIMeta(imeta: AudioMeta): TagArrayBuilder<T> {
@@ -43,7 +43,7 @@ class VoiceEvent(
mimeType: String?,
hash: String,
duration: Int,
waveform: List<Int>,
waveform: List<Float>,
) = build(AudioMeta(url, mimeType, hash, duration, waveform))
fun build(
@@ -64,7 +64,7 @@ class VoiceReplyEvent(
mimeType: String?,
hash: String,
duration: Int,
waveform: List<Int>,
waveform: List<Float>,
replyingTo: EventHintBundle<VoiceEvent>,
) = build(AudioMeta(url, mimeType, hash, duration, waveform), replyingTo)
@@ -22,11 +22,12 @@ package com.vitorpamplona.quartz.nipA0VoiceMessages.tags
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.ensure
import kotlin.collections.joinToString
class WaveformTag(
val wave: List<Int>,
val wave: List<Float>,
) {
fun toTagArray() = assemble(wave)
fun toTagArray() = assembleFloat(wave)
companion object {
const val TAG_NAME = "waveform"
@@ -35,26 +36,32 @@ class WaveformTag(
fun parse(tag: Array<String>): WaveformTag? = parseWave(tag)?.let { WaveformTag(it) }
@JvmStatic
fun parseWave(tag: Array<String>): List<Int>? {
fun parseWave(tag: Array<String>): List<Float>? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
val wave = tag[1].split(" ").mapNotNull { it.toIntOrNull() }
val wave = tag[1].split(" ").mapNotNull { it.toFloatOrNull() }
if (wave.isEmpty()) return null
return wave
}
fun parseWave(wave: String): List<Int>? {
val wave = wave.split(" ").mapNotNull { it.toIntOrNull() }
fun parseWave(wave: String): List<Float>? {
val wave = wave.split(" ").mapNotNull { it.toFloatOrNull() }
if (wave.isEmpty()) return null
return wave
}
@JvmStatic
fun assembleWave(wave: List<Int>) = wave.joinToString(" ")
fun assembleWaveInt(wave: List<Int>) = wave.joinToString(" ")
@JvmStatic
fun assemble(wave: List<Int>) = arrayOf(TAG_NAME, assembleWave(wave))
fun assembleInt(wave: List<Int>) = arrayOf(TAG_NAME, assembleWaveInt(wave))
@JvmStatic
fun assembleWaveFloat(wave: List<Float>) = wave.joinToString(" ")
@JvmStatic
fun assembleFloat(wave: List<Float>) = arrayOf(TAG_NAME, assembleWaveFloat(wave))
}
}
@@ -31,18 +31,24 @@ class LargeSoftCache<K, V> : CacheOperations<K, V> {
fun get(key: K): V? {
val softRef = cache.get(key)
val value = softRef?.get()
if (softRef == null) return null
val value = softRef.get()
return if (value != null) {
value
} else {
cache.remove(key)
cache.remove(key, softRef)
null
}
}
fun remove(key: K) = cache.remove(key)
fun removeIf(
key: K,
value: WeakReference<V>,
) = cache.remove(key, value)
override fun size() = cache.size
fun isEmpty() = cache.isEmpty()
@@ -79,13 +85,21 @@ class LargeSoftCache<K, V> : CacheOperations<K, V> {
builder: (key: K) -> V,
): V {
val softRef = cache[key]
val value = softRef?.get()
return if (value != null) {
value
} else {
return if (softRef == null) {
val newObject = builder(key)
cache.putIfAbsent(key, WeakReference(newObject))?.get() ?: newObject
} else {
val value = softRef.get()
if (value != null) {
value
} else {
// removes first to make sure the putIfAbsent works.
// another thread may put in between
cache.remove(key, softRef)
val newObject = builder(key)
return cache.putIfAbsent(key, WeakReference(newObject))?.get() ?: newObject
}
}
}
@@ -95,14 +109,14 @@ class LargeSoftCache<K, V> : CacheOperations<K, V> {
* this method can be called periodically or when memory pressure is high.
*/
fun cleanUp() {
val keysToRemove = mutableListOf<K>()
forEach { key, softRef ->
if (softRef == null) {
keysToRemove.add(key)
val keysToRemove = mutableMapOf<K, WeakReference<V>>()
cache.forEach { key, softRef ->
if (softRef.get() == null) {
keysToRemove.put(key, softRef)
}
}
keysToRemove.forEach { key ->
cache.remove(key)
keysToRemove.forEach { key, value ->
cache.remove(key, value)
println("Cleaned up entry for key: $key (object was garbage collected)")
}
println("Cache cleanup completed. Remaining size: ${cache.size}")
@@ -122,7 +136,7 @@ class LargeSoftCache<K, V> : CacheOperations<K, V> {
) {
val value = ref.get()
if (value == null) {
cache.remove(k)
cache.removeIf(k, ref)
} else {
inner.accept(k, value)
}
@@ -28,6 +28,7 @@ object TimeUtils {
const val ONE_HOUR = 60 * ONE_MINUTE
const val EIGHT_HOURS = 8 * ONE_HOUR
const val ONE_DAY = 24 * ONE_HOUR
const val NINETY_DAYS = 90 * ONE_DAY
const val ONE_WEEK = 7 * ONE_DAY
const val ONE_MONTH = 30 * ONE_DAY
const val ONE_YEAR = 365 * ONE_DAY
@@ -61,4 +62,6 @@ object TimeUtils {
fun oneMonthAgo() = now() - ONE_MONTH
fun randomWithTwoDays() = now() - RandomInstance.int(twoDays())
fun ninetyDaysFromNow() = now() + NINETY_DAYS
}