diff --git a/app/build.gradle b/app/build.gradle index c91e93ea9..25f1ed849 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,7 @@ plugins { - id 'com.android.application' - id 'org.jetbrains.kotlin.android' - id 'com.google.gms.google-services' + alias(libs.plugins.androidApplication) + alias(libs.plugins.jetbrainsKotlinAndroid) + alias(libs.plugins.googleServices) } android { @@ -145,14 +145,13 @@ android { // Should match compose version : https://developer.android.com/jetpack/androidx/releases/compose-kotlin kotlinCompilerExtensionVersion "1.5.8" } - packagingOptions { resources { - excludes += '/META-INF/{AL2.0,LGPL2.1}' + excludes += ['/META-INF/{AL2.0,LGPL2.1}', '**/libscrypt.dylib'] } - exclude '**/libscrypt.dylib' } + lint { disable 'MissingTranslation' } @@ -165,75 +164,78 @@ android { dependencies { implementation project(path: ':quartz') implementation project(path: ':commons') - implementation "androidx.core:core-ktx:$core_ktx_version" - implementation 'androidx.activity:activity-compose:1.8.2' - implementation "androidx.compose.ui:ui:$compose_ui_version" - implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version" + implementation libs.androidx.core.ktx + implementation libs.androidx.activity.compose + + implementation platform(libs.androidx.compose.bom) + + implementation libs.androidx.ui + implementation libs.androidx.ui.graphics + implementation libs.androidx.ui.tooling.preview // Needs this to open gallery / image upload - implementation "androidx.fragment:fragment-ktx:$fragment_version" + implementation libs.androidx.fragment.ktx // Navigation - implementation "androidx.navigation:navigation-compose:$nav_version" + implementation libs.androidx.navigation.compose // Observe Live data as State - implementation "androidx.compose.runtime:runtime-livedata:$compose_ui_version" + implementation libs.androidx.runtime.livedata // Material 3 Design - implementation "androidx.compose.material3:material3:${material3_version}" - implementation "androidx.compose.material:material-icons-extended:$compose_ui_version" + implementation libs.androidx.material3 + implementation libs.androidx.material.icons // Adaptive Layout / Two Pane - implementation "androidx.compose.material3:material3-window-size-class:${material3_version}" - implementation 'com.google.accompanist:accompanist-adaptive:0.34.0' - + implementation libs.androidx.material3.windowSize + implementation libs.accompanist.adaptive // Lifecycle - implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" + implementation libs.androidx.lifecycle.runtime.ktx + implementation libs.androidx.lifecycle.runtime.compose + implementation libs.androidx.lifecycle.viewmodel.compose + implementation libs.androidx.lifecycle.livedata.ktx // Zoomable images - implementation 'net.engawapg.lib:zoomable:1.6.0' + implementation libs.zoomable // Biometrics - implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05" + implementation libs.androidx.biometric.ktx // Websockets API - implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12' + implementation libs.okhttp // HTML Parsing for Link Preview - implementation 'org.jsoup:jsoup:1.17.2' + implementation libs.jsoup // Encrypted Key Storage - implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha06' + implementation libs.androidx.security.crypto.ktx // view videos - implementation "androidx.media3:media3-exoplayer:$media3_version" - implementation "androidx.media3:media3-exoplayer-hls:$media3_version" - implementation "androidx.media3:media3-ui:$media3_version" - implementation "androidx.media3:media3-session:$media3_version" + implementation libs.androidx.media3.exoplayer + implementation libs.androidx.media3.exoplayer.hls + implementation libs.androidx.media3.ui + implementation libs.androidx.media3.session // important for proxy / tor - implementation "androidx.media3:media3-datasource-okhttp:$media3_version" + implementation libs.androidx.media3.datasource.okhttp // Load images from the web. - implementation "io.coil-kt:coil-compose:$coil_version" + implementation libs.coil.compose // view gifs - implementation "io.coil-kt:coil-gif:$coil_version" + implementation libs.coil.gif // view svgs - implementation "io.coil-kt:coil-svg:$coil_version" + implementation libs.coil.svg // create blurhash - implementation group: 'io.trbl', name: 'blurhash', version: '1.0.0' + implementation libs.trbl.blurhash // Permission to upload pictures: - implementation "com.google.accompanist:accompanist-permissions:$accompanist_version" + implementation libs.accompanist.permissions // For QR generation - implementation 'com.google.zxing:core:3.5.3' - implementation 'com.journeyapps:zxing-android-embedded:4.3.0' + implementation libs.zxing + implementation libs.zxing.embedded // Markdown //implementation "com.halilibo.compose-richtext:richtext-ui:0.16.0" @@ -241,51 +243,50 @@ dependencies { //implementation "com.halilibo.compose-richtext:richtext-commonmark:0.16.0" // Markdown (With fix for full-image bleeds) - implementation('com.github.vitorpamplona.compose-richtext:richtext-ui:48702a8ced') - implementation('com.github.vitorpamplona.compose-richtext:richtext-ui-material3:48702a8ced') - implementation('com.github.vitorpamplona.compose-richtext:richtext-commonmark:48702a8ced') + implementation libs.markdown.ui + implementation libs.markdown.ui.material3 + implementation libs.markdown.commonmark // Language picker and Theme chooser - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation libs.androidx.appcompat // Local model for language identification - playImplementation 'com.google.mlkit:language-id:17.0.5' + playImplementation libs.google.mlkit.language.id // Google services model the translate text - playImplementation 'com.google.mlkit:translate:17.0.2' + playImplementation libs.google.mlkit.translate // PushNotifications - playImplementation platform('com.google.firebase:firebase-bom:32.7.2') - playImplementation 'com.google.firebase:firebase-messaging-ktx' + playImplementation platform(libs.firebase.bom) + playImplementation libs.firebase.messaging //PushNotifications(FDroid) - fdroidImplementation 'com.github.UnifiedPush:android-connector:2.2.0' + fdroidImplementation libs.unifiedpush // Charts - implementation "com.patrykandpatrick.vico:core:${vico_version}" - implementation "com.patrykandpatrick.vico:compose:${vico_version}" - implementation "com.patrykandpatrick.vico:views:${vico_version}" - implementation "com.patrykandpatrick.vico:compose-m2:${vico_version}" + implementation libs.vico.charts.core + implementation libs.vico.charts.compose + implementation libs.vico.charts.views + implementation libs.vico.charts.m3 // GeoHash - implementation 'com.github.drfonfon:android-kotlin-geohash:1.0' + implementation libs.drfonfon.geohash // Waveform visualizer - implementation 'com.github.lincollincol:compose-audiowaveform:1.1.1' + implementation libs.audiowaveform // Video compression lib - implementation 'com.github.AbedElazizShe:LightCompressor:1.3.2' + implementation libs.abedElazizShe.image.compressor // Image compression lib - implementation 'id.zelory:compressor:3.0.1' + implementation libs.zelory.video.compressor - testImplementation 'junit:junit:4.13.2' - testImplementation 'io.mockk:mockk:1.13.9' - androidTestImplementation 'androidx.test.ext:junit:1.2.0-alpha03' - androidTestImplementation 'androidx.test.ext:junit-ktx:1.2.0-alpha03' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version" - debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version" - debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version" + testImplementation libs.junit + testImplementation libs.mockk + androidTestImplementation libs.androidx.junit + androidTestImplementation libs.androidx.junit.ktx + androidTestImplementation libs.androidx.espresso.core + debugImplementation libs.androidx.ui.tooling + debugImplementation libs.androidx.ui.test.manifest } // https://gitlab.com/fdroid/wiki/-/wikis/HOWTO:-diff-&-fix-APKs-for-Reproducible-Builds#differing-assetsdexoptbaselineprofm-easy-to-fix diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 5e4d860e5..fc50dc25e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.model import android.util.Log +import android.util.LruCache import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.service.checkNotInMainThread @@ -1402,6 +1403,13 @@ object LocalCache { note.loadEvent(event, author, emptyList()) + event.editedNote()?.let { + getNoteIfExists(it)?.let { editedNote -> + modificationCache.remove(editedNote.idHex) + editedNote.liveSet?.innerModifications?.invalidateData() + } + } + refreshObservers(note) } @@ -1716,15 +1724,31 @@ object LocalCache { return minTime } + val modificationCache = LruCache>(20) + + fun cachedModificationEventsForNote(note: Note): List? { + return modificationCache[note.idHex] + } + suspend fun findLatestModificationForNote(note: Note): List { checkNotInMainThread() + + modificationCache[note.idHex]?.let { + return it + } + val time = TimeUtils.now() - return noteListCache.filter { item -> - val noteEvent = item.event + val newNotes = + noteListCache.filter { item -> + val noteEvent = item.event - noteEvent is TextNoteModificationEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time) - } + noteEvent is TextNoteModificationEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time) + }.sortedWith(compareBy({ it.createdAt() }, { it.idHex })) + + modificationCache.put(note.idHex, newNotes) + + return newNotes } fun cleanObservers() { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt index dc7d66d9b..eac7b0f8f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt @@ -37,26 +37,26 @@ class JsonFilter( val filter = factory.objectNode().apply { ids?.run { - put( + replace( "ids", factory.arrayNode(ids.size).apply { ids.forEach { add(it) } }, ) } authors?.run { - put( + replace( "authors", factory.arrayNode(authors.size).apply { authors.forEach { add(it) } }, ) } kinds?.run { - put( + replace( "kinds", factory.arrayNode(kinds.size).apply { kinds.forEach { add(it) } }, ) } tags?.run { entries.forEach { kv -> - put( + replace( "#${kv.key}", factory.arrayNode(kv.value.size).apply { kv.value.forEach { add(it) } }, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt index eefe41b99..9de8de758 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt @@ -47,7 +47,7 @@ data class Subscription( return factory.objectNode().apply { put("id", id) typedFilters?.also { filters -> - put( + replace( "typedFilters", factory.arrayNode(filters.size).apply { filters.forEach { filter -> add(filter.toJsonObject()) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt index 84ef11d26..ec3c9e05c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt @@ -36,8 +36,8 @@ class TypedFilter( val factory = Event.mapper.nodeFactory return factory.objectNode().apply { - put("types", typesToJson(types)) - put("filter", filterToJson(filter)) + replace("types", typesToJson(types)) + replace("filter", filterToJson(filter)) } } @@ -50,26 +50,26 @@ class TypedFilter( val factory = Event.mapper.nodeFactory return factory.objectNode().apply { filter.ids?.run { - put( + replace( "ids", factory.arrayNode(filter.ids.size).apply { filter.ids.forEach { add(it) } }, ) } filter.authors?.run { - put( + replace( "authors", factory.arrayNode(filter.authors.size).apply { filter.authors.forEach { add(it) } }, ) } filter.kinds?.run { - put( + replace( "kinds", factory.arrayNode(filter.kinds.size).apply { filter.kinds.forEach { add(it) } }, ) } filter.tags?.run { entries.forEach { kv -> - put( + replace( "#${kv.key}", factory.arrayNode(kv.value.size).apply { kv.value.forEach { add(it) } }, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 32b3b853f..fc9a92a09 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -61,6 +61,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable +import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -1088,10 +1089,58 @@ fun InnerNoteWithReactions( } @Stable -class EditState( - val showOriginal: MutableState = mutableStateOf(false), - val modificationsInOrder: MutableState> = mutableStateOf(emptyList()), -) +class EditState() { + private var modificationsList: List = persistentListOf() + private var modificationToShowIndex: Int = -1 + + val modificationToShow: MutableState = mutableStateOf(null) + val showingVersion: MutableState = mutableStateOf(0) + + fun hasModificationsToShow(): Boolean = modificationsList.isNotEmpty() + + fun isOriginal(): Boolean = modificationToShowIndex < 0 + + fun isLatest(): Boolean = modificationToShowIndex == modificationsList.lastIndex + + fun originalVersionId() = 0 + + fun lastVersionId() = modificationsList.size + + fun versionId() = modificationToShowIndex + 1 + + fun nextModification() { + if (modificationToShowIndex < 0) { + modificationToShowIndex = 0 + modificationToShow.value = modificationsList.getOrNull(0) + } else { + modificationToShowIndex++ + if (modificationToShowIndex >= modificationsList.size) { + modificationToShowIndex = -1 + modificationToShow.value = null + } else { + modificationToShow.value = modificationsList.getOrNull(modificationToShowIndex) + } + } + + showingVersion.value = versionId() + } + + fun updateModifications(newModifications: List) { + if (modificationsList != newModifications) { + modificationsList = newModifications + + if (newModifications.isEmpty()) { + modificationToShow.value = null + modificationToShowIndex = -1 + } else { + modificationToShowIndex = newModifications.lastIndex + modificationToShow.value = newModifications.last() + } + } + + showingVersion.value = versionId() + } +} @Composable private fun NoteBody( @@ -1105,14 +1154,7 @@ private fun NoteBody( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - val editState by - produceState(initialValue = EditState(), key1 = baseNote) { - accountViewModel.findModificationEventsForNote(baseNote) { newModifications -> - if (value.modificationsInOrder.value != newModifications) { - value.modificationsInOrder.value = newModifications - } - } - } + val editState = observeEdits(baseNote = baseNote, accountViewModel = accountViewModel) FirstUserInfoRow( baseNote = baseNote, @@ -1168,7 +1210,7 @@ private fun RenderNoteRow( backgroundColor: MutableState, makeItShort: Boolean, canPreview: Boolean, - editState: EditState, + editState: State>, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -1358,7 +1400,7 @@ fun RenderTextEvent( makeItShort: Boolean, canPreview: Boolean, backgroundColor: MutableState, - editState: EditState, + editState: State>, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -1371,10 +1413,10 @@ fun RenderTextEvent( derivedStateOf { val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null } val newBody = - if (editState.showOriginal.value || editState.modificationsInOrder.value.isEmpty()) { - body + if (editState.value is GenericLoadable.Loaded) { + (editState.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value?.event?.content() ?: body } else { - editState.modificationsInOrder.value.firstOrNull()?.event?.content() ?: body + body } if (!subject.isNullOrBlank() && !newBody.split("\n")[0].contains(subject)) { @@ -2822,7 +2864,7 @@ fun DisplayLocation( fun FirstUserInfoRow( baseNote: Note, showAuthorPicture: Boolean, - editState: EditState, + editState: State>, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -2857,8 +2899,10 @@ fun FirstUserInfoRow( DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav) } - if (!editState.modificationsInOrder.value.isEmpty()) { - DisplayEditStatus(editState.showOriginal) + if (editState.value is GenericLoadable.Loaded) { + (editState.value as? GenericLoadable.Loaded)?.loaded?.let { + DisplayEditStatus(it) + } } TimeAgo(baseNote) @@ -2868,15 +2912,69 @@ fun FirstUserInfoRow( } @Composable -fun DisplayEditStatus(showOriginal: MutableState) { +fun observeEdits( + baseNote: Note, + accountViewModel: AccountViewModel, +): State> { + val editState = + remember(baseNote.idHex) { + val cached = accountViewModel.cachedModificationEventsForNote(baseNote) + mutableStateOf( + if (cached != null) { + if (cached.isEmpty()) { + GenericLoadable.Empty() + } else { + val state = EditState() + state.updateModifications(cached) + GenericLoadable.Loaded(state) + } + } else { + GenericLoadable.Loading() + }, + ) + } + + val updatedNote = baseNote.live().innerModifications.observeAsState() + + LaunchedEffect(key1 = updatedNote) { + updatedNote.value?.note?.let { + accountViewModel.findModificationEventsForNote(it) { newModifications -> + if (newModifications.isEmpty()) { + if (editState.value !is GenericLoadable.Empty) { + editState.value = GenericLoadable.Empty() + } + } else { + if (editState.value is GenericLoadable.Loaded) { + (editState.value as? GenericLoadable.Loaded)?.loaded?.updateModifications(newModifications) + } else { + val state = EditState() + state.updateModifications(newModifications) + editState.value = GenericLoadable.Loaded(state) + } + } + } + } + } + + return editState +} + +@Composable +fun DisplayEditStatus(editState: EditState) { ClickableText( text = - if (showOriginal.value) { - buildAnnotatedString { append(stringResource(id = R.string.original)) } - } else { - buildAnnotatedString { append(stringResource(id = R.string.edited)) } + buildAnnotatedString { + if (editState.showingVersion.value == editState.originalVersionId()) { + append(stringResource(id = R.string.original)) + } else if (editState.showingVersion.value == editState.lastVersionId()) { + append(stringResource(id = R.string.edited)) + } else { + append(stringResource(id = R.string.edited_number, editState.versionId())) + } }, - onClick = { showOriginal.value = !showOriginal.value }, + onClick = { + editState.nextModification() + }, style = LocalTextStyle.current.copy( color = MaterialTheme.colorScheme.placeholderText, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index bfbfa71c8..0c03ebd7a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -111,14 +111,17 @@ class PollNoteViewModel : ViewModel() { } tallies.forEach { - it.zappedValue.value = zappedPollOptionAmount(it.option) - it.tally.value = - if (totalZapped.compareTo(BigDecimal.ZERO) > 0) { - it.zappedValue.value.divide(totalZapped, 2, RoundingMode.HALF_UP) + val zappedValue = zappedPollOptionAmount(it.option) + val tallyValue = + if (totalZapped > BigDecimal.ZERO) { + zappedValue.divide(totalZapped, 2, RoundingMode.HALF_UP) } else { BigDecimal.ZERO } - it.consensusThreadhold.value = consensusThreshold != null && it.tally.value >= consensusThreshold!! + + it.zappedValue.value = zappedValue + it.tally.value = tallyValue + it.consensusThreadhold.value = consensusThreshold != null && tallyValue >= consensusThreshold!! it.zappedByLoggedIn.value = account?.userProfile()?.let { it1 -> cachedIsPollOptionZappedBy(it.option, it1) } ?: false } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index df856f55e..747b6c2e9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -483,7 +483,10 @@ fun NoteDropDownMenu( if (wantsToEditPost.value) { EditPostView( - onClose = { wantsToEditPost.value = false }, + onClose = { + popupExpanded.value = false + wantsToEditPost.value = false + }, edit = note, accountViewModel = accountViewModel, nav = nav, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index f65619417..c54c31228 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -58,7 +58,6 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -86,6 +85,7 @@ import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.InlineCarrousel import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status @@ -132,6 +132,7 @@ import com.vitorpamplona.amethyst.ui.note.RenderPostApproval import com.vitorpamplona.amethyst.ui.note.RenderRepost import com.vitorpamplona.amethyst.ui.note.RenderTextEvent import com.vitorpamplona.amethyst.ui.note.VideoDisplay +import com.vitorpamplona.amethyst.ui.note.observeEdits import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -377,20 +378,13 @@ fun NoteMaster( onClick = { showHiddenNote = true }, ) } else { - val editState by - produceState(initialValue = EditState(), key1 = baseNote) { - accountViewModel.findModificationEventsForNote(baseNote) { newModifications -> - if (value.modificationsInOrder.value != newModifications) { - value.modificationsInOrder.value = newModifications - } - } - } - Column( modifier .fillMaxWidth() .padding(top = 10.dp), ) { + val editState = observeEdits(baseNote = baseNote, accountViewModel = accountViewModel) + Row( modifier = Modifier @@ -421,8 +415,10 @@ fun NoteMaster( DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav) } - if (!editState.modificationsInOrder.value.isEmpty()) { - DisplayEditStatus(editState.showOriginal) + if (editState.value is GenericLoadable.Loaded) { + (editState.value as? GenericLoadable.Loaded)?.loaded?.let { + DisplayEditStatus(it) + } } Text( @@ -850,9 +846,9 @@ private fun RenderWikiHeaderForThreadPreview() { val accountViewModel = mockAccountViewModel() val nav: (String) -> Unit = {} - val editState by + val editState = remember { - mutableStateOf(EditState()) + mutableStateOf(GenericLoadable.Empty()) } runBlocking { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index f18c22e44..88ec1d47b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -932,6 +932,8 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } } + fun cachedModificationEventsForNote(note: Note) = LocalCache.cachedModificationEventsForNote(note) + suspend fun findModificationEventsForNote( note: Note, onResult: (List) -> Unit, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f56c9324b..74e89ae60 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,6 +51,7 @@ Boost boosted edited + edit #%1$s original Quote Fork diff --git a/benchmark/build.gradle b/benchmark/build.gradle index 61ab23cfb..be097e2d7 100644 --- a/benchmark/build.gradle +++ b/benchmark/build.gradle @@ -1,7 +1,7 @@ plugins { - id 'com.android.library' - id 'androidx.benchmark' - id 'org.jetbrains.kotlin.android' + alias(libs.plugins.androidLibrary) + alias(libs.plugins.jetbrainsKotlinAndroid) + alias(libs.plugins.androidBenchmark) } android { @@ -49,10 +49,10 @@ android { } dependencies { - androidTestImplementation 'androidx.test:runner:1.5.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.benchmark:benchmark-junit4:1.2.3' + androidTestImplementation libs.androidx.runner + androidTestImplementation libs.androidx.junit + androidTestImplementation libs.junit + androidTestImplementation libs.androidx.benchmark.junit4 androidTestImplementation project(path: ':quartz') androidTestImplementation project(path: ':commons') diff --git a/build.gradle b/build.gradle index ac635ba5f..121417ec7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,31 +1,13 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -buildscript { - ext { - fragment_version = "1.6.2" - lifecycle_version = '2.7.0' - compose_ui_version = '1.6.2' - nav_version = '2.7.7' - room_version = "2.4.3" - accompanist_version = '0.34.0' - coil_version = '2.6.0' - vico_version = '1.14.0' - media3_version = '1.2.1' - core_ktx_version = '1.12.0' - material3_version = '1.2.0' - } - dependencies { - classpath 'com.google.gms:google-services:4.4.1' - } -} - plugins { - id 'com.android.application' version '8.2.2' apply false - id 'com.android.library' version '8.2.2' apply false - id 'org.jetbrains.kotlin.android' version '1.9.22' apply false - id 'org.jetbrains.kotlin.jvm' version '1.9.22' apply false - id 'androidx.benchmark' version '1.2.3' apply false - id 'com.diffplug.spotless' version '6.25.0' apply false + alias(libs.plugins.androidApplication) apply false + alias(libs.plugins.jetbrainsKotlinAndroid) apply false + alias(libs.plugins.androidLibrary) apply false + alias(libs.plugins.jetbrainsKotlinJvm) apply false + alias(libs.plugins.androidBenchmark) apply false + alias(libs.plugins.diffplugSpotless) apply false + alias(libs.plugins.googleServices) apply false } subprojects { diff --git a/commons/build.gradle b/commons/build.gradle index a30b159dd..6adfe0abd 100644 --- a/commons/build.gradle +++ b/commons/build.gradle @@ -1,6 +1,6 @@ plugins { - id 'com.android.library' - id 'org.jetbrains.kotlin.android' + alias(libs.plugins.androidLibrary) + alias(libs.plugins.jetbrainsKotlinAndroid) } android { @@ -38,12 +38,13 @@ dependencies { implementation project(path: ':quartz') // Import @Immutable and @Stable - implementation "androidx.compose.ui:ui:$compose_ui_version" + implementation platform(libs.androidx.compose.bom) + implementation libs.androidx.ui // immutable collections to avoid recomposition - api('org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7') + api libs.kotlinx.collections.immutable - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + testImplementation libs.junit + androidTestImplementation libs.androidx.junit + androidTestImplementation libs.androidx.espresso.core } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..88a75dab3 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,124 @@ +[versions] +accompanistAdaptive = "0.34.0" +activityCompose = "1.8.2" +agp = "8.3.0" +androidKotlinGeohash = "1.0" +androidLifecycle = "2.7.0" +androidxJunit = "1.2.0-alpha03" +appcompat = "1.6.1" +audiowaveform = "1.1.1" +benchmark = "1.2.3" +benchmarkJunit4 = "1.2.3" +biometricKtx = "1.2.0-alpha05" +blurhash = "1.0.0" +coil = "2.6.0" +composeBom = "2024.02.01" +coreKtx = "1.12.0" +espressoCore = "3.5.1" +firebaseBom = "32.7.3" +fragmentKtx = "1.6.2" +gms = "4.4.1" +jacksonModuleKotlin = "2.16.1" +jna = "5.14.0" +jsoup = "1.17.2" +junit = "4.13.2" +kotlin = "1.9.22" +kotlinxCollectionsImmutable = "0.3.7" +languageId = "17.0.5" +lazysodiumAndroid = "5.1.0" +lightcompressor = "1.3.2" +markdown = "48702a8ced" +media3 = "1.2.1" +mockk = "1.13.9" +navigationCompose = "2.7.7" +okhttp = "5.0.0-alpha.12" +runner = "1.5.2" +secp256k1KmpJniAndroid = "0.14.0" +securityCryptoKtx = "1.1.0-alpha06" +spotless = "6.25.0" +translate = "17.0.2" +unifiedpush = "2.2.0" +urlDetector = "0.1.23" +vico-charts = "1.14.0" +zelory = "3.0.1" +zoomable = "1.6.0" +zxing = "3.5.3" +zxingAndroidEmbedded = "4.3.0" + +[libraries] +abedElazizShe-image-compressor = { group = "com.github.AbedElazizShe", name = "LightCompressor", version.ref = "lightcompressor" } +accompanist-adaptive = { group = "com.google.accompanist", name = "accompanist-adaptive", version.ref = "accompanistAdaptive" } +accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanistAdaptive" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +androidx-benchmark-junit4 = { group = "androidx.benchmark", name = "benchmark-junit4", version.ref = "benchmarkJunit4" } +androidx-biometric-ktx = { group = "androidx.biometric", name = "biometric-ktx", version.ref = "biometricKtx" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxJunit" } +androidx-junit-ktx = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "androidxJunit" } +androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "androidLifecycle" } +androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidLifecycle" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidLifecycle" } +androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidLifecycle" } +androidx-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-material3-windowSize = { group = "androidx.compose.material3", name = "material3-window-size-class" } +androidx-media3-datasource-okhttp = { group = "androidx.media3", name = "media3-datasource-okhttp", version.ref = "media3" } +androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" } +androidx-media3-exoplayer-hls = { group = "androidx.media3", name = "media3-exoplayer-hls", version.ref = "media3" } +androidx-media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" } +androidx-media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } +androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" } +androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" } +androidx-runtime-runtime = { group = "androidx.compose.runtime", name = "runtime" } +androidx-security-crypto-ktx = { group = "androidx.security", name = "security-crypto-ktx", version.ref = "securityCryptoKtx" } +androidx-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +audiowaveform = { group = "com.github.lincollincol", name = "compose-audiowaveform", version.ref = "audiowaveform" } +coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } +coil-gif = { group = "io.coil-kt", name = "coil-gif", version.ref = "coil" } +coil-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coil" } +drfonfon-geohash = { group = "com.github.drfonfon", name = "android-kotlin-geohash", version.ref = "androidKotlinGeohash" } +firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } +firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging-ktx" } +google-mlkit-language-id = { group = "com.google.mlkit", name = "language-id", version.ref = "languageId" } +google-mlkit-translate = { group = "com.google.mlkit", name = "translate", version.ref = "translate" } +jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jacksonModuleKotlin" } +jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" } +jsoup = { group = "org.jsoup", name = "jsoup", version.ref = "jsoup" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" } +lazysodium-android = { group = "com.goterl", name = "lazysodium-android", version.ref = "lazysodiumAndroid" } +markdown-commonmark = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-commonmark", version.ref = "markdown" } +markdown-ui = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui", version.ref = "markdown" } +markdown-ui-material3 = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui-material3", version.ref = "markdown" } +mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +secp256k1-kmp-jni-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1KmpJniAndroid" } +trbl-blurhash = { group = "io.trbl", name = "blurhash", version.ref = "blurhash" } +unifiedpush = { group = "com.github.UnifiedPush", name = "android-connector", version.ref = "unifiedpush" } +url-detector = { group = "io.github.url-detector", name = "url-detector", version.ref = "urlDetector" } +vico-charts-compose = { group = "com.patrykandpatrick.vico", name = "compose", version.ref = "vico-charts" } +vico-charts-core = { group = "com.patrykandpatrick.vico", name = "core", version.ref = "vico-charts" } +vico-charts-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico-charts" } +vico-charts-views = { group = "com.patrykandpatrick.vico", name = "views", version.ref = "vico-charts" } +zelory-video-compressor = { group = "id.zelory", name = "compressor", version.ref = "zelory" } +zoomable = { group = "net.engawapg.lib", name = "zoomable", version.ref = "zoomable" } +zxing = { group = "com.google.zxing", name = "core", version.ref = "zxing" } +zxing-embedded = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxingAndroidEmbedded" } + +[plugins] +androidApplication = { id = "com.android.application", version.ref = "agp" } +androidBenchmark = { id = "androidx.benchmark", version.ref = "benchmark" } +androidLibrary = { id = "com.android.library", version.ref = "agp" } +diffplugSpotless = { id = "com.diffplug.spotless", version.ref = "spotless" } +googleServices = { id = "com.google.gms.google-services", version.ref = "gms" } +jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +jetbrainsKotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df554462d..c738a045f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Jan 04 09:23:50 EST 2023 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/quartz/build.gradle b/quartz/build.gradle index 48bc3289f..a9aa482f5 100644 --- a/quartz/build.gradle +++ b/quartz/build.gradle @@ -1,6 +1,6 @@ plugins { - id 'com.android.library' - id 'org.jetbrains.kotlin.android' + alias(libs.plugins.androidLibrary) + alias(libs.plugins.jetbrainsKotlinAndroid) } android { @@ -33,36 +33,39 @@ android { jvmTarget = '17' freeCompilerArgs += "-Xstring-concat=inline" } - packagingOptions { - // delete native scrypt lib because we cannot use dylibs in android - exclude '**/libscrypt.dylib' + resources { + excludes += ['**/libscrypt.dylib'] + } } } dependencies { - implementation "androidx.core:core-ktx:$core_ktx_version" + implementation libs.androidx.core.ktx + + implementation platform(libs.androidx.compose.bom) // @Immutable and @Stable - implementation "androidx.compose.runtime:runtime:$compose_ui_version" + implementation libs.androidx.runtime.runtime // Bitcoin secp256k1 bindings to Android - api 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.14.0' + api libs.secp256k1.kmp.jni.android // LibSodium for ChaCha encryption (NIP-44) + // Wait for @aar support in version catalogs implementation "com.goterl:lazysodium-android:5.1.0@aar" implementation 'net.java.dev.jna:jna:5.14.0@aar' // Performant Parser of JSONs into Events - api 'com.fasterxml.jackson.module:jackson-module-kotlin:2.16.1' + api libs.jackson.module.kotlin // immutable collections to avoid recomposition - api('org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7') + api libs.kotlinx.collections.immutable // Parses URLs from Text: - api "io.github.url-detector:url-detector:0.1.23" + api libs.url.detector - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + testImplementation libs.junit + androidTestImplementation libs.androidx.junit + androidTestImplementation libs.androidx.espresso.core } \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Lud06.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Lud06.kt index d4d2bb178..775a7c85f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Lud06.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Lud06.kt @@ -30,7 +30,7 @@ class Lud06 { fun toLud16(str: String): String? { return try { - val url = toLnUrlp(str) + val url = toLnUrlp(str) ?: return null val matcher = LNURLP_PATTERN.matcher(url) if (matcher.find()) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt index de2e719b3..31267b345 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt @@ -94,7 +94,9 @@ object Nip19Bech32 { val key = matcher.group(3) // bech32 val additionalChars = matcher.group(4) // additional chars - return parseComponents(type!!, key, additionalChars) + if (type == null) return null + + return parseComponents(type, key, additionalChars) } catch (e: Throwable) { Log.e("NIP19 Parser", "Issue trying to Decode NIP19 $uri: ${e.message}", e) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt index 26a522e49..a6d202298 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt @@ -105,14 +105,16 @@ open class BaseTextNoteEvent( val additionalChars = matcher2.group(4) // additional chars try { - val parsed = Nip19Bech32.parseComponents(type, key, additionalChars)?.entity + if (type != null) { + val parsed = Nip19Bech32.parseComponents(type, key, additionalChars)?.entity - if (parsed != null) { - if (parsed is Nip19Bech32.NProfile) { - returningList.add(parsed.hex) - } - if (parsed is Nip19Bech32.NPub) { - returningList.add(parsed.hex) + if (parsed != null) { + if (parsed is Nip19Bech32.NProfile) { + returningList.add(parsed.hex) + } + if (parsed is Nip19Bech32.NPub) { + returningList.add(parsed.hex) + } } } } catch (e: Exception) { @@ -151,14 +153,16 @@ open class BaseTextNoteEvent( val key = matcher2.group(3) // bech32 val additionalChars = matcher2.group(4) // additional chars - val parsed = Nip19Bech32.parseComponents(type, key, additionalChars)?.entity + if (type != null) { + val parsed = Nip19Bech32.parseComponents(type, key, additionalChars)?.entity - if (parsed != null) { - when (parsed) { - is Nip19Bech32.NEvent -> citations.add(parsed.hex) - is Nip19Bech32.NAddress -> citations.add(parsed.atag) - is Nip19Bech32.Note -> citations.add(parsed.hex) - is Nip19Bech32.NEmbed -> citations.add(parsed.event.id) + if (parsed != null) { + when (parsed) { + is Nip19Bech32.NEvent -> citations.add(parsed.hex) + is Nip19Bech32.NAddress -> citations.add(parsed.atag) + is Nip19Bech32.Note -> citations.add(parsed.hex) + is Nip19Bech32.NEmbed -> citations.add(parsed.event.id) + } } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt index aba51ab92..1d8549679 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -386,7 +386,7 @@ open class Event( put("pubkey", pubKey) put("created_at", createdAt) put("kind", kind) - put( + replace( "tags", factory.arrayNode(tags.size).apply { tags.forEach { tag -> diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt index 55f0b4b2b..ae2d56396 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt @@ -63,7 +63,7 @@ interface EventInterface { fun isTaggedUser(idHex: String): Boolean - fun isTaggedUsers(idHex: Set): Boolean + fun isTaggedUsers(idHexes: Set): Boolean fun isTaggedEvent(idHex: String): Boolean diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitPatchEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitPatchEvent.kt index d9a6670e1..4c5a04ce8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitPatchEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitPatchEvent.kt @@ -60,7 +60,7 @@ class GitPatchEvent( fun commitPGPSig() = tags.firstOrNull { it.size > 1 && it[0] == "commit-pgp-sig" }?.get(1) fun committer() = - tags.filter { it.size > 1 && it[0] == "committer" }?.mapNotNull { + tags.filter { it.size > 1 && it[0] == "committer" }.mapNotNull { Committer(it.getOrNull(1), it.getOrNull(2), it.getOrNull(3), it.getOrNull(4)) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt index 377dbc91d..79018b85a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt @@ -34,6 +34,8 @@ class TextNoteModificationEvent( content: String, sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + fun editedNote() = firstTaggedEvent() + companion object { const val KIND = 1010 const val ALT = "Content Change Event" diff --git a/settings.gradle b/settings.gradle index 6ac181c95..45a4461c7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,8 +1,14 @@ pluginManagement { repositories { - gradlePluginPortal() - google() + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } mavenCentral() + gradlePluginPortal() maven { url "https://jitpack.io" content { @@ -11,6 +17,7 @@ pluginManagement { } } } + dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { @@ -19,6 +26,7 @@ dependencyResolutionManagement { maven { url "https://jitpack.io" } } } + rootProject.name = "Amethyst" include ':app' include ':benchmark'