Adds local cache to wait for OTS confirmations
Presents OTS pending and confirmations on the screen Adds Menu item to OTS any post.
This commit is contained in:
@@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.service.HttpClientManager
|
|||||||
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||||
|
import com.vitorpamplona.quartz.encoders.HexKey
|
||||||
import com.vitorpamplona.quartz.encoders.Nip47WalletConnect
|
import com.vitorpamplona.quartz.encoders.Nip47WalletConnect
|
||||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||||
@@ -113,6 +114,7 @@ private object PrefKeys {
|
|||||||
const val AUTOMATICALLY_SHOW_PROFILE_PICTURE = "automatically_show_profile_picture"
|
const val AUTOMATICALLY_SHOW_PROFILE_PICTURE = "automatically_show_profile_picture"
|
||||||
const val SIGNER_PACKAGE_NAME = "signer_package_name"
|
const val SIGNER_PACKAGE_NAME = "signer_package_name"
|
||||||
const val HAS_DONATED_IN_VERSION = "has_donated_in_version"
|
const val HAS_DONATED_IN_VERSION = "has_donated_in_version"
|
||||||
|
const val PENDING_ATTESTATIONS = "pending_attestations"
|
||||||
|
|
||||||
const val ALL_ACCOUNT_INFO = "all_saved_accounts_info"
|
const val ALL_ACCOUNT_INFO = "all_saved_accounts_info"
|
||||||
const val SHARED_SETTINGS = "shared_settings"
|
const val SHARED_SETTINGS = "shared_settings"
|
||||||
@@ -339,6 +341,11 @@ object LocalPreferences {
|
|||||||
} else {
|
} else {
|
||||||
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!)
|
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
putString(
|
||||||
|
PrefKeys.PENDING_ATTESTATIONS,
|
||||||
|
Event.mapper.writeValueAsString(account.pendingAttestations),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
@@ -556,6 +563,26 @@ object LocalPreferences {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val pendingAttestations =
|
||||||
|
try {
|
||||||
|
getString(PrefKeys.PENDING_ATTESTATIONS, null)?.let {
|
||||||
|
println("Decoding Attestation List: " + it)
|
||||||
|
if (it != null) {
|
||||||
|
Event.mapper.readValue<Map<HexKey, String>>(it)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
Log.w(
|
||||||
|
"LocalPreferences",
|
||||||
|
"Error Decoding Contact List ${getString(PrefKeys.LATEST_CONTACT_LIST, null)}",
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
val languagePreferences =
|
val languagePreferences =
|
||||||
try {
|
try {
|
||||||
getString(PrefKeys.LANGUAGE_PREFS, null)?.let {
|
getString(PrefKeys.LANGUAGE_PREFS, null)?.let {
|
||||||
@@ -649,6 +676,7 @@ object LocalPreferences {
|
|||||||
filterSpamFromStrangers = filterSpam,
|
filterSpamFromStrangers = filterSpam,
|
||||||
lastReadPerRoute = lastReadPerRoute,
|
lastReadPerRoute = lastReadPerRoute,
|
||||||
hasDonatedInVersion = hasDonatedInVersion,
|
hasDonatedInVersion = hasDonatedInVersion,
|
||||||
|
pendingAttestations = pendingAttestations ?: emptyMap(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Loads from DB
|
// Loads from DB
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
|||||||
import com.vitorpamplona.quartz.events.MetadataEvent
|
import com.vitorpamplona.quartz.events.MetadataEvent
|
||||||
import com.vitorpamplona.quartz.events.MuteListEvent
|
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||||
import com.vitorpamplona.quartz.events.NIP24Factory
|
import com.vitorpamplona.quartz.events.NIP24Factory
|
||||||
|
import com.vitorpamplona.quartz.events.OtsEvent
|
||||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.Price
|
import com.vitorpamplona.quartz.events.Price
|
||||||
@@ -185,6 +186,7 @@ class Account(
|
|||||||
var filterSpamFromStrangers: Boolean = true,
|
var filterSpamFromStrangers: Boolean = true,
|
||||||
var lastReadPerRoute: Map<String, Long> = mapOf<String, Long>(),
|
var lastReadPerRoute: Map<String, Long> = mapOf<String, Long>(),
|
||||||
var hasDonatedInVersion: Set<String> = setOf<String>(),
|
var hasDonatedInVersion: Set<String> = setOf<String>(),
|
||||||
|
var pendingAttestations: Map<HexKey, String> = mapOf<HexKey, String>(),
|
||||||
val scope: CoroutineScope = Amethyst.instance.applicationIOScope,
|
val scope: CoroutineScope = Amethyst.instance.applicationIOScope,
|
||||||
) {
|
) {
|
||||||
var transientHiddenUsers: ImmutableSet<String> = persistentSetOf()
|
var transientHiddenUsers: ImmutableSet<String> = persistentSetOf()
|
||||||
@@ -893,6 +895,38 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun updateAttestations() {
|
||||||
|
Log.d("Pending Attestations", "Updating ${pendingAttestations.size} pending attestations")
|
||||||
|
|
||||||
|
pendingAttestations.toMap().forEach { pair ->
|
||||||
|
val newAttestation = OtsEvent.upgrade(pair.value, pair.key)
|
||||||
|
|
||||||
|
if (pair.value != newAttestation) {
|
||||||
|
OtsEvent.create(pair.key, newAttestation, signer) {
|
||||||
|
LocalCache.justConsume(it, null)
|
||||||
|
Client.send(it)
|
||||||
|
|
||||||
|
pendingAttestations = pendingAttestations - pair.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasPendingAttestations(note: Note): Boolean {
|
||||||
|
val id = note.event?.id() ?: note.idHex
|
||||||
|
return pendingAttestations.get(id) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun timestamp(note: Note) {
|
||||||
|
if (!isWriteable()) return
|
||||||
|
|
||||||
|
val id = note.event?.id() ?: note.idHex
|
||||||
|
|
||||||
|
pendingAttestations = pendingAttestations + Pair(id, OtsEvent.stamp(id))
|
||||||
|
|
||||||
|
saveable.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
fun follow(user: User) {
|
fun follow(user: User) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
|||||||
import com.vitorpamplona.quartz.events.MetadataEvent
|
import com.vitorpamplona.quartz.events.MetadataEvent
|
||||||
import com.vitorpamplona.quartz.events.MuteListEvent
|
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||||
import com.vitorpamplona.quartz.events.NNSEvent
|
import com.vitorpamplona.quartz.events.NNSEvent
|
||||||
|
import com.vitorpamplona.quartz.events.OtsEvent
|
||||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||||
import com.vitorpamplona.quartz.events.PinListEvent
|
import com.vitorpamplona.quartz.events.PinListEvent
|
||||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||||
@@ -716,6 +717,28 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consume(
|
||||||
|
event: OtsEvent,
|
||||||
|
relay: Relay?,
|
||||||
|
) {
|
||||||
|
val version = getOrCreateNote(event.id)
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (version.event?.id() == event.id()) return
|
||||||
|
|
||||||
|
if (version.event == null) {
|
||||||
|
// makes sure the OTS has a valid certificate
|
||||||
|
if (event.cacheVerify() == null) return // no valid OTS
|
||||||
|
|
||||||
|
version.loadEvent(event, author, emptyList())
|
||||||
|
|
||||||
|
version.liveSet?.innerOts?.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshObservers(version)
|
||||||
|
}
|
||||||
|
|
||||||
fun consume(
|
fun consume(
|
||||||
event: BadgeDefinitionEvent,
|
event: BadgeDefinitionEvent,
|
||||||
relay: Relay?,
|
relay: Relay?,
|
||||||
@@ -1626,6 +1649,25 @@ object LocalCache {
|
|||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun findEarliestOtsForNote(note: Note): Long? {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
val validOts =
|
||||||
|
notes
|
||||||
|
.mapNotNull {
|
||||||
|
val noteEvent = it.value.event
|
||||||
|
if ((noteEvent is OtsEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpired())) {
|
||||||
|
noteEvent.verifiedTime
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validOts.isEmpty()) return null
|
||||||
|
|
||||||
|
return validOts.minBy { it }
|
||||||
|
}
|
||||||
|
|
||||||
fun cleanObservers() {
|
fun cleanObservers() {
|
||||||
notes.forEach { it.value.clearLive() }
|
notes.forEach { it.value.clearLive() }
|
||||||
|
|
||||||
@@ -1960,6 +2002,7 @@ object LocalCache {
|
|||||||
is MetadataEvent -> consume(event)
|
is MetadataEvent -> consume(event)
|
||||||
is MuteListEvent -> consume(event, relay)
|
is MuteListEvent -> consume(event, relay)
|
||||||
is NNSEvent -> comsume(event, relay)
|
is NNSEvent -> comsume(event, relay)
|
||||||
|
is OtsEvent -> consume(event, relay)
|
||||||
is PrivateDmEvent -> consume(event, relay)
|
is PrivateDmEvent -> consume(event, relay)
|
||||||
is PinListEvent -> consume(event, relay)
|
is PinListEvent -> consume(event, relay)
|
||||||
is PeopleListEvent -> consume(event, relay)
|
is PeopleListEvent -> consume(event, relay)
|
||||||
|
|||||||
@@ -947,6 +947,7 @@ class NoteLiveSet(u: Note) {
|
|||||||
val innerReports = NoteBundledRefresherLiveData(u)
|
val innerReports = NoteBundledRefresherLiveData(u)
|
||||||
val innerRelays = NoteBundledRefresherLiveData(u)
|
val innerRelays = NoteBundledRefresherLiveData(u)
|
||||||
val innerZaps = NoteBundledRefresherLiveData(u)
|
val innerZaps = NoteBundledRefresherLiveData(u)
|
||||||
|
val innerOts = NoteBundledRefresherLiveData(u)
|
||||||
|
|
||||||
val metadata = innerMetadata.map { it }
|
val metadata = innerMetadata.map { it }
|
||||||
val reactions = innerReactions.map { it }
|
val reactions = innerReactions.map { it }
|
||||||
@@ -1011,6 +1012,7 @@ class NoteLiveSet(u: Note) {
|
|||||||
innerReports.destroy()
|
innerReports.destroy()
|
||||||
innerRelays.destroy()
|
innerRelays.destroy()
|
||||||
innerZaps.destroy()
|
innerZaps.destroy()
|
||||||
|
innerOts.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
|||||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||||
|
import com.vitorpamplona.quartz.events.OtsEvent
|
||||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.ReactionEvent
|
import com.vitorpamplona.quartz.events.ReactionEvent
|
||||||
import com.vitorpamplona.quartz.events.ReportEvent
|
import com.vitorpamplona.quartz.events.ReportEvent
|
||||||
@@ -134,6 +135,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
|||||||
ReportEvent.KIND,
|
ReportEvent.KIND,
|
||||||
LnZapEvent.KIND,
|
LnZapEvent.KIND,
|
||||||
PollNoteEvent.KIND,
|
PollNoteEvent.KIND,
|
||||||
|
OtsEvent.KIND,
|
||||||
),
|
),
|
||||||
tags = mapOf("e" to it.map { it.idHex }),
|
tags = mapOf("e" to it.map { it.idHex }),
|
||||||
since = findMinimumEOSEs(it),
|
since = findMinimumEOSEs(it),
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
|
|||||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||||
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
||||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||||
import com.vitorpamplona.amethyst.ui.components.LoadThumbAndThenVideoView
|
import com.vitorpamplona.amethyst.ui.components.LoadThumbAndThenVideoView
|
||||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||||
@@ -177,6 +178,7 @@ import com.vitorpamplona.amethyst.ui.theme.boostedNoteModifier
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier
|
import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||||
import com.vitorpamplona.amethyst.ui.theme.nip05
|
import com.vitorpamplona.amethyst.ui.theme.nip05
|
||||||
@@ -2544,9 +2546,43 @@ fun SecondUserInfoRow(
|
|||||||
Spacer(StdHorzSpacer)
|
Spacer(StdHorzSpacer)
|
||||||
DisplayPoW(pow)
|
DisplayPoW(pow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DisplayOts(note, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DisplayOts(
|
||||||
|
note: Note,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
LoadOts(
|
||||||
|
note,
|
||||||
|
accountViewModel,
|
||||||
|
whenConfirmed = {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val timeStr by remember(note) { mutableStateOf(timeAgo(it, context = context)) }
|
||||||
|
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.existed_since, timeStr),
|
||||||
|
color = MaterialTheme.colorScheme.lessImportantLink,
|
||||||
|
fontSize = Font14SP,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
whenPending = {
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.timestamp_pending_short),
|
||||||
|
color = MaterialTheme.colorScheme.lessImportantLink,
|
||||||
|
fontSize = Font14SP,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ShowForkInformation(
|
private fun ShowForkInformation(
|
||||||
noteEvent: BaseTextNoteEvent,
|
noteEvent: BaseTextNoteEvent,
|
||||||
@@ -2638,6 +2674,37 @@ fun LoadStatuses(
|
|||||||
content(statuses)
|
content(statuses)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoadOts(
|
||||||
|
note: Note,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
whenConfirmed: @Composable (Long) -> Unit,
|
||||||
|
whenPending: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
var earliestDate: GenericLoadable<Long> by remember { mutableStateOf(GenericLoadable.Loading()) }
|
||||||
|
|
||||||
|
val noteStatus by note.live().innerOts.observeAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = noteStatus) {
|
||||||
|
accountViewModel.findOtsEventsForNote(noteStatus?.note ?: note) { newOts ->
|
||||||
|
if (newOts == null) {
|
||||||
|
earliestDate = GenericLoadable.Empty()
|
||||||
|
} else {
|
||||||
|
earliestDate = GenericLoadable.Loaded(newOts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(earliestDate as? GenericLoadable.Loaded)?.let {
|
||||||
|
whenConfirmed(it.loaded)
|
||||||
|
} ?: run {
|
||||||
|
val account = accountViewModel.account.saveable.observeAsState()
|
||||||
|
if (account.value?.account?.hasPendingAttestations(note) == true) {
|
||||||
|
whenPending()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoadCityName(
|
fun LoadCityName(
|
||||||
geohash: GeoHash,
|
geohash: GeoHash,
|
||||||
|
|||||||
@@ -342,11 +342,9 @@ private fun RenderMainPopup(
|
|||||||
icon = ImageVector.vectorResource(id = R.drawable.relays),
|
icon = ImageVector.vectorResource(id = R.drawable.relays),
|
||||||
label = stringResource(R.string.broadcast),
|
label = stringResource(R.string.broadcast),
|
||||||
) {
|
) {
|
||||||
scope.launch(Dispatchers.IO) {
|
accountViewModel.broadcast(note)
|
||||||
accountViewModel.broadcast(note)
|
// showSelectTextDialog = true
|
||||||
// showSelectTextDialog = true
|
onDismiss()
|
||||||
onDismiss()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
VerticalDivider(primaryLight)
|
VerticalDivider(primaryLight)
|
||||||
NoteQuickActionItem(
|
NoteQuickActionItem(
|
||||||
|
|||||||
@@ -554,13 +554,28 @@ fun NoteDropDownMenu(
|
|||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.broadcast)) },
|
text = { Text(stringResource(R.string.broadcast)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch(Dispatchers.IO) {
|
accountViewModel.broadcast(note)
|
||||||
accountViewModel.broadcast(note)
|
onDismiss()
|
||||||
onDismiss()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Divider()
|
Divider()
|
||||||
|
if (accountViewModel.account.hasPendingAttestations(note)) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.timestamp_pending)) },
|
||||||
|
onClick = {
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.timestamp_it)) },
|
||||||
|
onClick = {
|
||||||
|
accountViewModel.timestamp(note)
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
if (state.isPrivateBookmarkNote) {
|
if (state.isPrivateBookmarkNote) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.remove_from_private_bookmarks)) },
|
text = { Text(stringResource(R.string.remove_from_private_bookmarks)) },
|
||||||
|
|||||||
+24
-1
@@ -528,7 +528,22 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun broadcast(note: Note) {
|
fun broadcast(note: Note) {
|
||||||
account.broadcast(note)
|
viewModelScope.launch(Dispatchers.IO) { account.broadcast(note) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun timestamp(note: Note) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) { account.timestamp(note) }
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastTimeItTriedToUpdateAttestations: Long = 0
|
||||||
|
|
||||||
|
fun upgradeAttestations() {
|
||||||
|
// only tries to upgrade every hour
|
||||||
|
val now = TimeUtils.now()
|
||||||
|
if (now - lastTimeItTriedToUpdateAttestations > TimeUtils.ONE_HOUR) {
|
||||||
|
lastTimeItTriedToUpdateAttestations = now
|
||||||
|
viewModelScope.launch(Dispatchers.IO) { account.updateAttestations() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(note: Note) {
|
fun delete(note: Note) {
|
||||||
@@ -889,6 +904,13 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
viewModelScope.launch(Dispatchers.IO) { onResult(LocalCache.findStatusesForUser(myUser)) }
|
viewModelScope.launch(Dispatchers.IO) { onResult(LocalCache.findStatusesForUser(myUser)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun findOtsEventsForNote(
|
||||||
|
note: Note,
|
||||||
|
onResult: (Long?) -> Unit,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) { onResult(LocalCache.findEarliestOtsForNote(note)) }
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun checkGetOrCreateChannel(key: HexKey): Channel? {
|
private suspend fun checkGetOrCreateChannel(key: HexKey): Channel? {
|
||||||
return LocalCache.checkGetOrCreateChannel(key)
|
return LocalCache.checkGetOrCreateChannel(key)
|
||||||
}
|
}
|
||||||
@@ -1100,6 +1122,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
"Notification Dots Calculation refresh ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()}",
|
"Notification Dots Calculation refresh ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()}",
|
||||||
)
|
)
|
||||||
invalidateInsertData(newNotes)
|
invalidateInsertData(newNotes)
|
||||||
|
upgradeAttestations()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@
|
|||||||
<string name="copy_user_pubkey">Copy Author ID</string>
|
<string name="copy_user_pubkey">Copy Author ID</string>
|
||||||
<string name="copy_note_id">Copy Note ID</string>
|
<string name="copy_note_id">Copy Note ID</string>
|
||||||
<string name="broadcast">Broadcast</string>
|
<string name="broadcast">Broadcast</string>
|
||||||
|
<string name="timestamp_it">Timestamp it</string>
|
||||||
|
<string name="timestamp_pending">Timestamp: Pending Confirmations</string>
|
||||||
|
<string name="timestamp_pending_short">OTS: Pending</string>
|
||||||
<string name="request_deletion">Request Deletion</string>
|
<string name="request_deletion">Request Deletion</string>
|
||||||
<string name="block_report">Block / Report</string>
|
<string name="block_report">Block / Report</string>
|
||||||
<string name="block_hide_user"><![CDATA[Block & Hide User]]></string>
|
<string name="block_hide_user"><![CDATA[Block & Hide User]]></string>
|
||||||
@@ -782,4 +785,5 @@
|
|||||||
<string name="git_repository">Git Repository: %1$s</string>
|
<string name="git_repository">Git Repository: %1$s</string>
|
||||||
<string name="git_web_address">Web:</string>
|
<string name="git_web_address">Web:</string>
|
||||||
<string name="git_clone_address">Clone:</string>
|
<string name="git_clone_address">Clone:</string>
|
||||||
|
<string name="existed_since">OTS: %1$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ import com.vitorpamplona.quartz.events.Event
|
|||||||
import com.vitorpamplona.quartz.events.OtsEvent
|
import com.vitorpamplona.quartz.events.OtsEvent
|
||||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||||
import junit.framework.TestCase.assertEquals
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import junit.framework.TestCase.fail
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
@@ -60,9 +60,33 @@ class OtsTest {
|
|||||||
val ots = Event.Companion.fromJson(otsPendingEvent) as OtsEvent
|
val ots = Event.Companion.fromJson(otsPendingEvent) as OtsEvent
|
||||||
println(ots.info())
|
println(ots.info())
|
||||||
assertEquals(null, ots.verify())
|
assertEquals(null, ots.verify())
|
||||||
|
|
||||||
|
val eventId =
|
||||||
|
ots.digestEvent() ?: run {
|
||||||
|
fail("Should not be null")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val upgraded = OtsEvent.upgrade(ots.content, eventId)
|
||||||
|
|
||||||
|
val signer = NostrSignerInternal(KeyPair())
|
||||||
|
|
||||||
|
var newOts: OtsEvent? = null
|
||||||
|
val countDownLatch = CountDownLatch(1)
|
||||||
|
|
||||||
|
OtsEvent.create(eventId, upgraded, signer) {
|
||||||
|
newOts = it
|
||||||
|
countDownLatch.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
|
||||||
|
|
||||||
|
println(newOts!!.toJson())
|
||||||
|
println(newOts!!.info())
|
||||||
|
|
||||||
|
assertEquals(1708879025L, newOts!!.verify())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Ignore("Need to figure out a way to wait for Bitcoin confirmations to test thiss")
|
|
||||||
@Test
|
@Test
|
||||||
fun createOTSEventAndVerify() {
|
fun createOTSEventAndVerify() {
|
||||||
val signer = NostrSignerInternal(KeyPair())
|
val signer = NostrSignerInternal(KeyPair())
|
||||||
@@ -70,7 +94,7 @@ class OtsTest {
|
|||||||
|
|
||||||
val countDownLatch = CountDownLatch(1)
|
val countDownLatch = CountDownLatch(1)
|
||||||
|
|
||||||
OtsEvent.create(otsEvent2Digest, signer) {
|
OtsEvent.create(otsEvent2Digest, OtsEvent.stamp(otsEvent2Digest), signer) {
|
||||||
ots = it
|
ots = it
|
||||||
countDownLatch.countDown()
|
countDownLatch.countDown()
|
||||||
}
|
}
|
||||||
@@ -80,6 +104,6 @@ class OtsTest {
|
|||||||
println(ots!!.toJson())
|
println(ots!!.toJson())
|
||||||
println(ots!!.info())
|
println(ots!!.info())
|
||||||
|
|
||||||
assertEquals(1706322179L, ots!!.verify())
|
assertEquals(null, ots!!.verify())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ class OtsEvent(
|
|||||||
content: String,
|
content: String,
|
||||||
sig: HexKey,
|
sig: HexKey,
|
||||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
@Transient
|
||||||
|
var verifiedTime: Long? = null
|
||||||
|
|
||||||
fun digestEvent() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
|
fun digestEvent() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
|
||||||
|
|
||||||
fun digest() = digestEvent()?.hexToByteArray()
|
fun digest() = digestEvent()?.hexToByteArray()
|
||||||
@@ -53,25 +56,19 @@ class OtsEvent(
|
|||||||
return Base64.getDecoder().decode(content)
|
return Base64.getDecoder().decode(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun verify(): Long? {
|
fun cacheVerify(): Long? {
|
||||||
try {
|
return if (verifiedTime != null) {
|
||||||
val digest = digest() ?: return null
|
verifiedTime
|
||||||
|
} else {
|
||||||
val detachedOts = DetachedTimestampFile.deserialize(otsByteArray())
|
verifiedTime = verify()
|
||||||
|
verifiedTime
|
||||||
val result = otsInstance.verify(detachedOts, digest)
|
|
||||||
if (result == null || result.isEmpty()) {
|
|
||||||
return null
|
|
||||||
} else {
|
|
||||||
return result.get(VerifyResult.Chains.BITCOIN)?.timestamp
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e is CancellationException) throw e
|
|
||||||
Log.e("OpenTimeStamps", "Failed to verify", e)
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun verify(): Long? {
|
||||||
|
return digestEvent()?.let { OtsEvent.verify(otsByteArray(), it) }
|
||||||
|
}
|
||||||
|
|
||||||
fun info(): String {
|
fun info(): String {
|
||||||
val detachedOts = DetachedTimestampFile.deserialize(otsByteArray())
|
val detachedOts = DetachedTimestampFile.deserialize(otsByteArray())
|
||||||
return otsInstance.info(detachedOts)
|
return otsInstance.info(detachedOts)
|
||||||
@@ -83,27 +80,77 @@ class OtsEvent(
|
|||||||
|
|
||||||
var otsInstance = OpenTimestamps(BlockstreamExplorer(), CalendarPureJavaBuilder())
|
var otsInstance = OpenTimestamps(BlockstreamExplorer(), CalendarPureJavaBuilder())
|
||||||
|
|
||||||
fun stamp(eventId: HexKey): ByteArray {
|
fun stamp(eventId: HexKey): String {
|
||||||
val hash = Hash(eventId.hexToByteArray(), OpSHA256._TAG)
|
val hash = Hash(eventId.hexToByteArray(), OpSHA256._TAG)
|
||||||
val file = DetachedTimestampFile.from(hash)
|
val file = DetachedTimestampFile.from(hash)
|
||||||
val timestamp = otsInstance.stamp(file)
|
val timestamp = otsInstance.stamp(file)
|
||||||
val detachedToSerialize = DetachedTimestampFile(hash.getOp(), timestamp)
|
val detachedToSerialize = DetachedTimestampFile(hash.getOp(), timestamp)
|
||||||
return detachedToSerialize.serialize()
|
return Base64.getEncoder().encodeToString(detachedToSerialize.serialize())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun upgrade(
|
||||||
|
otsFile: String,
|
||||||
|
eventId: HexKey,
|
||||||
|
): String {
|
||||||
|
val detachedOts = DetachedTimestampFile.deserialize(Base64.getDecoder().decode(otsFile))
|
||||||
|
|
||||||
|
return if (otsInstance.upgrade(detachedOts)) {
|
||||||
|
// if the change is now verifiable.
|
||||||
|
if (verify(detachedOts, eventId) != null) {
|
||||||
|
Base64.getEncoder().encodeToString(detachedOts.serialize())
|
||||||
|
} else {
|
||||||
|
otsFile
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
otsFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verify(
|
||||||
|
otsFile: String,
|
||||||
|
eventId: HexKey,
|
||||||
|
): Long? {
|
||||||
|
return verify(Base64.getDecoder().decode(otsFile), eventId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verify(
|
||||||
|
otsFile: ByteArray,
|
||||||
|
eventId: HexKey,
|
||||||
|
): Long? {
|
||||||
|
return verify(DetachedTimestampFile.deserialize(otsFile), eventId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verify(
|
||||||
|
detachedOts: DetachedTimestampFile,
|
||||||
|
eventId: HexKey,
|
||||||
|
): Long? {
|
||||||
|
try {
|
||||||
|
val result = otsInstance.verify(detachedOts, eventId.hexToByteArray())
|
||||||
|
if (result == null || result.isEmpty()) {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
return result.get(VerifyResult.Chains.BITCOIN)?.timestamp
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
Log.e("OpenTimeStamps", "Failed to verify", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun create(
|
fun create(
|
||||||
eventId: HexKey,
|
eventId: HexKey,
|
||||||
|
otsFileBase64: String,
|
||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
onReady: (OtsEvent) -> Unit,
|
onReady: (OtsEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
val otsFile = stamp(eventId)
|
|
||||||
val tags =
|
val tags =
|
||||||
arrayOf(
|
arrayOf(
|
||||||
arrayOf("e", eventId),
|
arrayOf("e", eventId),
|
||||||
arrayOf("alt", ALT),
|
arrayOf("alt", ALT),
|
||||||
)
|
)
|
||||||
signer.sign(createdAt, KIND, tags, Base64.getEncoder().encodeToString(otsFile), onReady)
|
signer.sign(createdAt, KIND, tags, otsFileBase64, onReady)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user