mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
Merge branch 'vitorpamplona:main' into profiles-list-management
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -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
|
||||
|
||||
|
||||
+4
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -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)
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
|
||||
|
||||
@Immutable
|
||||
class WaveformData(
|
||||
val wave: List<Int>,
|
||||
val wave: List<Float>,
|
||||
)
|
||||
|
||||
@Composable
|
||||
|
||||
+43
-3
@@ -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()
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ fun NewUserMetadataScreen(
|
||||
postViewModel.init(accountViewModel)
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(accountViewModel) {
|
||||
LaunchedEffect(postViewModel, accountViewModel) {
|
||||
postViewModel.load()
|
||||
}
|
||||
|
||||
|
||||
+6
-4
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+9
-7
@@ -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 {
|
||||
|
||||
+54
-1
@@ -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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -104,7 +104,7 @@ fun ReplyCommentPostScreen(
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(postViewModel, accountViewModel) {
|
||||
reply?.let {
|
||||
postViewModel.reply(it)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+3
-1
@@ -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,
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+6
-6
@@ -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)))
|
||||
}
|
||||
|
||||
+11
@@ -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,
|
||||
|
||||
+5
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -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,
|
||||
|
||||
+5
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -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,
|
||||
|
||||
+5
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -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()
|
||||
|
||||
|
||||
+10
-7
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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()
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ fun NewCommunityNoteButton(
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
val route =
|
||||
Route.NewPost(
|
||||
Route.NewShortNote(
|
||||
baseReplyTo = note.idHex,
|
||||
)
|
||||
nav.nav(route)
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@ fun NewProductScreen(
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(postViewModel, accountViewModel) {
|
||||
postViewModel.reloadRelaySet()
|
||||
draft?.let {
|
||||
postViewModel.editFromDraft(it)
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ fun GeoHashPostScreen(
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(postViewModel, accountViewModel) {
|
||||
geohash?.let {
|
||||
postViewModel.newPostFor(GeohashId(it))
|
||||
}
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ fun HashtagPostScreen(
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(postViewModel, accountViewModel) {
|
||||
hashtag?.let {
|
||||
postViewModel.newPostFor(HashtagId(it))
|
||||
}
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+8
-8
@@ -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)))
|
||||
}
|
||||
|
||||
+43
-6
@@ -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))
|
||||
|
||||
+5
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -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(
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ fun AddDMRelayListDialog(
|
||||
|
||||
postViewModel.init(accountViewModel)
|
||||
|
||||
LaunchedEffect(postViewModel, accountViewModel.account) {
|
||||
LaunchedEffect(postViewModel, accountViewModel) {
|
||||
postViewModel.load()
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -431,7 +431,7 @@ fun ReactionsColumn(
|
||||
iconSize = Size40dp,
|
||||
onQuotePress = {
|
||||
nav.nav(
|
||||
Route.NewPost(
|
||||
Route.NewShortNote(
|
||||
quote = baseNote.idHex,
|
||||
),
|
||||
)
|
||||
|
||||
+8
@@ -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))
|
||||
) &&
|
||||
|
||||
+18
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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))
|
||||
|
||||
+3
-3
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
+16
-12
@@ -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()
|
||||
|
||||
+3
-3
@@ -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())
|
||||
|
||||
|
||||
+2
-2
@@ -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,
|
||||
)
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
-18
@@ -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,
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+1
-1
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
-22
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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()
|
||||
|
||||
|
||||
+3
-1
@@ -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)
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
|
||||
+15
-8
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user