merge with recent changes to main

This commit is contained in:
toadlyBroodle 2023-04-01 09:54:13 +09:00
commit 1f08f33600
18 changed files with 134 additions and 65 deletions

View File

@ -12,8 +12,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 33
versionCode 109
versionName "0.30.2"
versionCode 113
versionName "0.31.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.model
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.model.*
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.toShortenHex
@ -46,7 +47,7 @@ open class Note(val idHex: String) {
var relays = setOf<String>()
private set
var lastReactionsDownloadTime: Map<String, Long> = emptyMap()
var lastReactionsDownloadTime: Map<String, EOSETime> = emptyMap()
fun id() = Hex.decode(idHex)
open fun idNote() = id().toNote()

View File

@ -53,6 +53,11 @@ class ThreadAssembler {
val threadRoot = searchRoot(note, thread) ?: note
loadDown(threadRoot, thread)
// adds the replies of the note in case the search for Root
// did not added them.
note.replies.forEach {
loadDown(it, thread)
}
thread.toSet()
} else {

View File

@ -3,10 +3,6 @@ package com.vitorpamplona.amethyst.model
import com.baha.url.preview.BahaUrlPreview
import com.baha.url.preview.IUrlPreviewCallback
import com.baha.url.preview.UrlInfoItem
import com.vitorpamplona.amethyst.ui.components.imageExtension
import com.vitorpamplona.amethyst.ui.components.isValidURL
import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator
import com.vitorpamplona.amethyst.ui.components.videoExtension
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -47,29 +43,4 @@ object UrlCachedPreviewer {
).fetchUrlPreview()
}
}
fun findUrlsInMessage(message: String): List<String> {
return message.split('\n').map { paragraph ->
paragraph.split(' ').filter { word: String ->
isValidURL(word) || noProtocolUrlValidator.matcher(word).matches()
}
}.flatten()
}
fun preloadPreviewsFor(note: Note) {
note.event?.content()?.let {
findUrlsInMessage(it).forEach {
val removedParamsFromUrl = it.split("?")[0].lowercase()
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
// Preload Images? Isn't this too heavy?
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
// Do nothing for now.
} else if (isValidURL(removedParamsFromUrl)) {
previewInfo(it)
} else {
previewInfo("https://$it")
}
}
}
}
}

View File

@ -7,6 +7,7 @@ import com.vitorpamplona.amethyst.service.model.ContactListEvent
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.MetadataEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.toShortenHex
@ -31,7 +32,7 @@ class User(val pubkeyHex: String) {
var reports = mapOf<User, Set<Note>>()
private set
var latestEOSEs: Map<String, Long> = emptyMap()
var latestEOSEs: Map<String, EOSETime> = emptyMap()
var zaps = mapOf<Note, Note?>()
private set

View File

@ -13,6 +13,7 @@ import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
@ -20,6 +21,8 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
object NostrAccountDataSource : NostrDataSource("AccountData") {
lateinit var account: Account
val latestEOSEs = EOSEAccount()
fun createAccountContactListFilter(): TypedFilter {
return TypedFilter(
types = FeedType.values().toSet(),
@ -69,7 +72,8 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
types = FeedType.values().toSet(),
filter = JsonFilter(
kinds = listOf(ReportEvent.kind),
authors = listOf(account.userProfile().pubkeyHex)
authors = listOf(account.userProfile().pubkeyHex),
since = latestEOSEs.users[account.userProfile()]?.relayList
)
)
}
@ -88,11 +92,14 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
BadgeAwardEvent.kind
),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
limit = 200
limit = 400,
since = latestEOSEs.users[account.userProfile()]?.relayList
)
)
val accountChannel = requestNewChannel()
val accountChannel = requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), relayUrl, time)
}
override fun updateChannelFilters() {
// gets everthing about the user logged in

View File

@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
@ -12,11 +13,14 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
lateinit var account: Account
val latestEOSEs = EOSEAccount()
fun createMessagesToMeFilter() = TypedFilter(
types = setOf(FeedType.PRIVATE_DMS),
filter = JsonFilter(
kinds = listOf(PrivateDmEvent.kind),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex))
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
since = latestEOSEs.users[account.userProfile()]?.relayList
)
)
@ -24,7 +28,8 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
types = setOf(FeedType.PRIVATE_DMS),
filter = JsonFilter(
kinds = listOf(PrivateDmEvent.kind),
authors = listOf(account.userProfile().pubkeyHex)
authors = listOf(account.userProfile().pubkeyHex),
since = latestEOSEs.users[account.userProfile()]?.relayList
)
)
@ -32,7 +37,8 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
types = setOf(FeedType.PUBLIC_CHATS),
filter = JsonFilter(
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind),
authors = listOf(account.userProfile().pubkeyHex)
authors = listOf(account.userProfile().pubkeyHex),
since = latestEOSEs.users[account.userProfile()]?.relayList
)
)
@ -40,7 +46,8 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
types = FeedType.values().toSet(), // Metadata comes from any relay
filter = JsonFilter(
kinds = listOf(ChannelCreateEvent.kind),
ids = account.followingChannels.toList()
ids = account.followingChannels.toList(),
since = latestEOSEs.users[account.userProfile()]?.relayList
)
)
@ -64,13 +71,16 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
filter = JsonFilter(
kinds = listOf(ChannelMessageEvent.kind),
tags = mapOf("e" to listOf(it)),
since = latestEOSEs.users[account.userProfile()]?.relayList,
limit = 25 // Remember to consider spam that is being removed from the UI
)
)
}
}
val chatroomListChannel = requestNewChannel()
val chatroomListChannel = requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), relayUrl, time)
}
override fun updateChannelFilters() {
val list = listOf(

View File

@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
@ -16,6 +17,8 @@ import kotlinx.coroutines.launch
object NostrHomeDataSource : NostrDataSource("HomeFeed") {
lateinit var account: Account
val latestEOSEs = EOSEAccount()
private val cacheListener: (UserState) -> Unit = {
invalidateFilters()
}
@ -54,7 +57,8 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
filter = JsonFilter(
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind),
authors = followSet,
limit = 400
limit = 400,
since = latestEOSEs.users[account.userProfile()]?.relayList
)
)
}
@ -73,12 +77,15 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 100
limit = 100,
since = latestEOSEs.users[account.userProfile()]?.relayList
)
)
}
val followAccountChannel = requestNewChannel()
val followAccountChannel = requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), relayUrl, time)
}
override fun updateChannelFilters() {
followAccountChannel.typedFilters = listOfNotNull(createFollowAccountsFilter(), createFollowTagsFilter()).ifEmpty { null }

View File

@ -2,7 +2,20 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.*
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
@ -127,8 +140,14 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
val singleEventChannel = requestNewChannel { time, relayUrl ->
eventsToWatch.forEach {
it.lastReactionsDownloadTime = it.lastReactionsDownloadTime + Pair(relayUrl, time)
val eose = it.lastReactionsDownloadTime[relayUrl]
if (eose == null) {
it.lastReactionsDownloadTime = it.lastReactionsDownloadTime + Pair(relayUrl, EOSETime(time))
} else {
eose.time = time
}
}
// Many relays operate with limits in the amount of filters.
// As information comes, the filters will be rotated to get more data.
invalidateFilters()

View File

@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.MetadataEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
@ -42,8 +43,14 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
val userChannel = requestNewChannel() { time, relayUrl ->
usersToWatch.forEach {
it.latestEOSEs = it.latestEOSEs + Pair(relayUrl, time)
val eose = it.latestEOSEs[relayUrl]
if (eose == null) {
it.latestEOSEs = it.latestEOSEs + Pair(relayUrl, EOSETime(time))
} else {
eose.time = time
}
}
// Many relays operate with limits in the amount of filters.
// As information comes, the filters will be rotated to get more data.
invalidateFilters()

View File

@ -0,0 +1,27 @@
package com.vitorpamplona.amethyst.service.relays
import com.vitorpamplona.amethyst.model.User
class EOSETime(var time: Long)
class EOSERelayList(var relayList: Map<String, EOSETime> = emptyMap()) {
fun addOrUpdate(relayUrl: String, time: Long) {
val eose = relayList[relayUrl]
if (eose == null) {
relayList = relayList + Pair(relayUrl, EOSETime(time))
} else {
eose.time = time
}
}
}
class EOSEAccount(var users: Map<User, EOSERelayList> = emptyMap()) {
fun addOrUpdate(user: User, relayUrl: String, time: Long) {
val relayList = users[user]
if (relayList == null) {
users = users + mapOf(user to EOSERelayList(mapOf(relayUrl to EOSETime(time))))
} else {
relayList.addOrUpdate(relayUrl, time)
}
}
}

View File

@ -4,14 +4,13 @@ import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import java.util.*
class JsonFilter(
val ids: List<String>? = null,
val authors: List<String>? = null,
val kinds: List<Int>? = null,
val tags: Map<String, List<String>>? = null,
val since: Map<String, Long>? = null,
val since: Map<String, EOSETime>? = null,
val until: Long? = null,
val limit: Int? = null,
val search: String? = null
@ -37,7 +36,7 @@ class JsonFilter(
if (forRelay != null) {
val relaySince = get(forRelay)
if (relaySince != null) {
jsonObject.addProperty("since", relaySince)
jsonObject.addProperty("since", relaySince.time)
}
} else {
val jsonObjectSince = JsonObject()

View File

@ -40,6 +40,8 @@ class Relay(
var closingTime = 0L
var afterEOSE = false
fun register(listener: Listener) {
listeners = listeners.plus(listener)
}
@ -74,6 +76,7 @@ class Relay(
val listener = object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
afterEOSE = false
isReady = true
ping = response.receivedResponseAtMillis - response.sentRequestAtMillis
// Log.w("Relay", "Relay OnOpen, Loading All subscriptions $url")
@ -89,12 +92,19 @@ class Relay(
val msg = Event.gson.fromJson(text, JsonElement::class.java).asJsonArray
val type = msg[0].asString
val channel = msg[1].asString
when (type) {
"EVENT" -> {
// Log.w("Relay", "Relay onEVENT $url, $channel")
listeners.forEach { it.onEvent(this@Relay, channel, Event.fromJson(msg[2], Client.lenient)) }
listeners.forEach {
it.onEvent(this@Relay, channel, Event.fromJson(msg[2], Client.lenient))
if (afterEOSE) {
it.onRelayStateChange(this@Relay, Type.EOSE, channel)
}
}
}
"EOSE" -> listeners.forEach {
afterEOSE = true
// Log.w("Relay", "Relay onEOSE $url, $channel")
it.onRelayStateChange(this@Relay, Type.EOSE, channel)
}
@ -136,6 +146,7 @@ class Relay(
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
socket = null
isReady = false
afterEOSE = false
closingTime = Date().time / 1000
listeners.forEach { it.onRelayStateChange(this@Relay, Type.DISCONNECT, null) }
}
@ -147,6 +158,7 @@ class Relay(
// Failures disconnect the relay.
socket = null
isReady = false
afterEOSE = false
closingTime = Date().time / 1000
Log.w("Relay", "Relay onFailure $url, ${response?.message} $response")
@ -161,6 +173,7 @@ class Relay(
} catch (e: Exception) {
errorCounter++
isReady = false
afterEOSE = false
closingTime = Date().time / 1000
Log.e("Relay", "Relay Invalid $url")
e.printStackTrace()
@ -173,6 +186,7 @@ class Relay(
socket?.close(1000, "Normal close")
socket = null
isReady = false
afterEOSE = false
}
fun sendFilter(requestId: String) {
@ -186,6 +200,7 @@ class Relay(
// println("FILTERSSENT $url $request")
socket?.send(request)
eventUploadCounterInBytes += request.bytesUsedInMemory()
afterEOSE = false
}
}
} else {

View File

@ -138,5 +138,5 @@ object ImageSaver {
MediaScannerConnection.scanFile(context, arrayOf(outputFile.toString()), null, null)
}
private const val PICTURES_SUBDIRECTORY = "Amethyst " + BuildConfig.VERSION_NAME
private const val PICTURES_SUBDIRECTORY = "Amethyst"
}

View File

@ -167,7 +167,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
if (isValidURL(myUrlPreview)) {
val removedParamsFromUrl =
myUrlPreview.split("?")[0].lowercase()
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
if (imageExtensions.any { removedParamsFromUrl.endsWith(it, true) }) {
AsyncImage(
model = myUrlPreview,
contentDescription = myUrlPreview,
@ -182,9 +182,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
RoundedCornerShape(15.dp)
)
)
} else if (videoExtension.matcher(removedParamsFromUrl)
.matches()
) {
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it, true) }) {
VideoView(myUrlPreview)
} else {
UrlPreview(myUrlPreview, myUrlPreview)

View File

@ -46,8 +46,8 @@ import java.net.URISyntaxException
import java.net.URL
import java.util.regex.Pattern
val imageExtension = Pattern.compile("(.*/)*.+\\.(png|jpg|gif|bmp|jpeg|webp|svg)$", Pattern.CASE_INSENSITIVE)
val videoExtension = Pattern.compile("(.*/)*.+\\.(mp4|avi|wmv|mpg|amv|webm|mov)$", Pattern.CASE_INSENSITIVE)
val imageExtensions = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg")
val videoExtensions = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov")
// Group 1 = url, group 4 additional chars
val noProtocolUrlValidator = Pattern.compile("(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+)*\\/?)(.*)")
@ -138,10 +138,10 @@ fun RichTextViewer(
// sequence of images will render in a slideview
if (isValidURL(word)) {
val removedParamsFromUrl = word.split("?")[0].lowercase()
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
if (imageExtensions.any { word.endsWith(it, true) }) {
imagesForPager.add(word)
}
if (videoExtension.matcher(removedParamsFromUrl).matches()) {
if (videoExtensions.any { word.endsWith(it, true) }) {
imagesForPager.add(word)
}
}
@ -160,9 +160,9 @@ fun RichTextViewer(
if (isValidURL(word)) {
val removedParamsFromUrl = word.split("?")[0].lowercase()
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
if (imageExtensions.any { word.endsWith(it, true) }) {
ZoomableImageView(word, imagesForPager)
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
} else if (videoExtensions.any { word.endsWith(it, true) }) {
ZoomableImageView(word, imagesForPager)
} else {
UrlPreview(word, "$word ")

View File

@ -62,7 +62,7 @@ fun ZoomableImageView(word: String, images: List<String> = listOf(word)) {
mutableStateOf<AsyncImagePainter.State?>(null)
}
if (imageExtension.matcher(word).matches()) {
if (imageExtensions.any { word.endsWith(it, true) }) {
AsyncImage(
model = word,
contentDescription = word,
@ -171,7 +171,7 @@ fun ZoomableImageDialog(imageUrl: String, allImages: List<String> = listOf(image
@Composable
private fun RenderImageOrVideo(imageUrl: String) {
if (imageExtension.matcher(imageUrl).matches()) {
if (imageExtensions.any { imageUrl.endsWith(it, true) }) {
AsyncImage(
model = imageUrl,
contentDescription = stringResource(id = R.string.profile_image),

View File

@ -23,7 +23,9 @@ object NotificationFeedFilter : FeedFilter<Note>() {
}
.filter { it ->
it.event !is TextNoteEvent ||
it.replyTo?.any { it.author == loggedInUser } == true ||
(it.event as? TextNoteEvent)?.taggedEvents()?.any {
LocalCache.checkGetOrCreateNote(it)?.author == loggedInUser
} == true ||
loggedInUser in it.directlyCiteUsers()
}
.filter { it ->