diff --git a/app/build.gradle b/app/build.gradle index 3f5de8711..d7de6c61b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 140 - versionName "0.40.6" + versionCode 144 + versionName "0.42.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 09e7bf954..3882218b1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -6,6 +6,8 @@ import android.content.SharedPreferences import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS +import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS import com.vitorpamplona.amethyst.model.RelaySetupInfo import com.vitorpamplona.amethyst.model.toByteArray import com.vitorpamplona.amethyst.service.model.ContactListEvent @@ -46,6 +48,8 @@ private object PrefKeys { const val ZAP_AMOUNTS = "zapAmounts" const val DEFAULT_ZAPTYPE = "defaultZapType" const val DEFAULT_FILE_SERVER = "defaultFileServer" + const val DEFAULT_HOME_FOLLOW_LIST = "defaultHomeFollowList" + const val DEFAULT_STORIES_FOLLOW_LIST = "defaultStoriesFollowList" const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer" const val LATEST_CONTACT_LIST = "latestContactList" const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog" @@ -197,6 +201,8 @@ object LocalPreferences { putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices)) putString(PrefKeys.DEFAULT_ZAPTYPE, gson.toJson(account.defaultZapType)) putString(PrefKeys.DEFAULT_FILE_SERVER, gson.toJson(account.defaultFileServer)) + putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, account.defaultHomeFollowList) + putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, account.defaultStoriesFollowList) putString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, gson.toJson(account.zapPaymentRequest)) putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList)) putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog) @@ -217,6 +223,8 @@ object LocalPreferences { val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf() val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language + val defaultHomeFollowList = getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: KIND3_FOLLOWS + val defaultStoriesFollowList = getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS val zapAmountChoices = gson.fromJson( getString(PrefKeys.ZAP_AMOUNTS, "[]"), @@ -278,6 +286,8 @@ object LocalPreferences { zapAmountChoices, defaultZapType, defaultFileServer, + defaultHomeFollowList, + defaultStoriesFollowList, zapPaymentRequestServer, hideDeleteRequestDialog, hideBlockAlertDialog, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index 1faebf214..480097a68 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -12,6 +12,7 @@ import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource import com.vitorpamplona.amethyst.service.NostrThreadDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource +import com.vitorpamplona.amethyst.service.NostrVideoDataSource import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Constants @@ -33,6 +34,7 @@ object ServiceManager { NostrAccountDataSource.account = myAccount NostrHomeDataSource.account = myAccount NostrChatroomListDataSource.account = myAccount + NostrVideoDataSource.account = myAccount // Notification Elements NostrHomeDataSource.start() @@ -61,6 +63,7 @@ object ServiceManager { NostrSingleUserDataSource.stop() NostrThreadDataSource.stop() NostrUserProfileDataSource.stop() + NostrVideoDataSource.stop() Client.disconnect() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 7d5952379..ddacd9993 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -38,6 +38,9 @@ fun getLanguagesSpokenByUser(): Set { return codedList } +val GLOBAL_FOLLOWS = " Global " +val KIND3_FOLLOWS = " All Follows " + @OptIn(DelicateCoroutinesApi::class) class Account( val loggedIn: Persona, @@ -50,6 +53,8 @@ class Account( var zapAmountChoices: List = listOf(500L, 1000L, 5000L), var defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PRIVATE, var defaultFileServer: ServersAvailable = ServersAvailable.IMGUR, + var defaultHomeFollowList: String = KIND3_FOLLOWS, + var defaultStoriesFollowList: String = GLOBAL_FOLLOWS, var zapPaymentRequest: Nip47URI? = null, var hideDeleteRequestDialog: Boolean = false, var hideBlockAlertDialog: Boolean = false, @@ -403,12 +408,12 @@ class Account( } } - fun createNip95(data: ByteArray, headerInfo: FileHeader): Pair? { + fun createNip95(byteArray: ByteArray, headerInfo: FileHeader): Pair? { if (!isWriteable()) return null val data = FileStorageEvent.create( mimeType = headerInfo.mimeType ?: "", - data = data, + data = byteArray, privateKey = loggedIn.privKey!! ) @@ -729,6 +734,18 @@ class Account( saveable.invalidateData() } + fun changeDefaultHomeFollowList(name: String) { + defaultHomeFollowList = name + live.invalidateData() + saveable.invalidateData() + } + + fun changeDefaultStoriesFollowList(name: String) { + defaultStoriesFollowList = name + live.invalidateData() + saveable.invalidateData() + } + fun changeZapAmounts(newAmounts: List) { zapAmountChoices = newAmounts live.invalidateData() @@ -741,6 +758,37 @@ class Account( saveable.invalidateData() } + fun selectedUsersFollowList(listName: String?): Set? { + if (listName == GLOBAL_FOLLOWS) return null + if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingKeySet() + + val privKey = loggedIn.privKey + + return if (listName != null) { + val aTag = ATag(PeopleListEvent.kind, userProfile().pubkeyHex, listName, null).toTag() + val list = LocalCache.addressables[aTag] + if (list != null) { + val publicHexList = (list.event as? PeopleListEvent)?.bookmarkedPeople() ?: emptySet() + val privateHexList = privKey?.let { + (list.event as? PeopleListEvent)?.privateTaggedUsers(it) + } ?: emptySet() + + (publicHexList + privateHexList).toSet() + } else { + emptySet() + } + } else { + emptySet() + } + } + + fun selectedTagsFollowList(listName: String?): Set? { + if (listName == GLOBAL_FOLLOWS) return null + if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingTagSet() + + return emptySet() + } + fun sendChangeChannel(name: String, about: String, picture: String, channel: Channel) { if (!isWriteable()) return diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt index 7439f2f8c..8312f1876 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt @@ -14,7 +14,7 @@ fun checkForHashtagWithIcon(tag: String): HashtagIcon? { "plebs", "pleb", "plebchain" -> HashtagIcon(R.drawable.plebs, "Pleb", Color.Unspecified, Modifier.padding(2.dp, 2.dp, 0.dp, 1.dp)) "coffee", "coffeechain", "cafe" -> HashtagIcon(R.drawable.coffee, "Coffee", Color.Unspecified, Modifier.padding(2.dp, 2.dp, 0.dp, 0.dp)) "skullofsatoshi" -> HashtagIcon(R.drawable.skull, "SkullofSatoshi", Color.Unspecified, Modifier.padding(2.dp, 1.dp, 0.dp, 0.dp)) - "grownostr", "#permaculture", "#gardening", "#garden" -> HashtagIcon(R.drawable.grownostr, "GrowNostr", Color.Unspecified, Modifier.padding(0.dp, 1.dp, 0.dp, 1.dp)) + "grownostr", "gardening", "garden" -> HashtagIcon(R.drawable.grownostr, "GrowNostr", Color.Unspecified, Modifier.padding(0.dp, 1.dp, 0.dp, 1.dp)) "footstr" -> HashtagIcon(R.drawable.footstr, "Footstr", Color.Unspecified, Modifier.padding(1.dp, 1.dp, 0.dp, 0.dp)) else -> null } 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 6dff6da3a..2ecfcf63e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -159,6 +159,20 @@ object LocalCache { } } + fun consume(event: PeopleListEvent) { + val note = getOrCreateAddressableNote(event.address()) + val author = getOrCreateUser(event.pubKey) + + // Already processed this event. + if (note.event?.id() == event.id()) return + + if (event.createdAt > (note.createdAt() ?: 0)) { + note.loadEvent(event, author, emptyList()) + + refreshObservers(note) + } + } + fun formattedDateTime(timestamp: Long): String { return Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault()) .format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a")) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index 3025a742f..45bbb4f1b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -62,9 +62,9 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { return TypedFilter( types = COMMON_FEED_TYPES, filter = JsonFilter( - kinds = listOf(BookmarkListEvent.kind), + kinds = listOf(BookmarkListEvent.kind, PeopleListEvent.kind), authors = listOf(account.userProfile().pubkeyHex), - limit = 1 + limit = 100 ) ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 8335a7177..5de8ef2bf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -90,6 +90,7 @@ abstract class NostrDataSource(val debugName: String) { is LongTextNoteEvent -> LocalCache.consume(event, relay) is MetadataEvent -> LocalCache.consume(event) is PrivateDmEvent -> LocalCache.consume(event, relay) + is PeopleListEvent -> LocalCache.consume(event) is ReactionEvent -> LocalCache.consume(event) is RecommendRelayEvent -> LocalCache.consume(event) is ReportEvent -> LocalCache.consume(event, relay) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt index 5f855bc7c..a4b5e8232 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -45,7 +45,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") { } fun createFollowAccountsFilter(): TypedFilter { - val follows = account.followingKeySet() + val follows = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet() val followKeys = follows.map { it.substring(0, 6) @@ -65,7 +65,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") { } fun createFollowTagsFilter(): TypedFilter? { - val hashToLoad = account.followingTagSet() + val hashToLoad = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet() if (hashToLoad.isEmpty()) return null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt index 5ad410390..6dd343e56 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.service +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.model.FileHeaderEvent import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent import com.vitorpamplona.amethyst.service.relays.FeedType @@ -7,17 +8,47 @@ import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter object NostrVideoDataSource : NostrDataSource("VideoFeed") { - fun createGlobalFilter() = TypedFilter( - types = setOf(FeedType.GLOBAL), - filter = JsonFilter( - kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind), - limit = 200 + lateinit var account: Account + + fun createContextualFilter(): TypedFilter? { + val follows = account.selectedUsersFollowList(account.defaultStoriesFollowList) + + val followKeys = follows?.map { + it.substring(0, 6) + } + + return TypedFilter( + types = setOf(FeedType.GLOBAL), + filter = JsonFilter( + authors = followKeys, + kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind), + limit = 200 + ) ) - ) + } + + fun createFollowTagsFilter(): TypedFilter? { + val hashToLoad = account.selectedTagsFollowList(account.defaultStoriesFollowList) + + if (hashToLoad.isNullOrEmpty()) return null + + return TypedFilter( + types = setOf(FeedType.GLOBAL), + filter = JsonFilter( + kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind), + tags = mapOf( + "t" to hashToLoad.map { + listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) + }.flatten() + ), + limit = 100 + ) + ) + } val videoFeedChannel = requestNewChannel() override fun updateChannelFilters() { - videoFeedChannel.typedFilters = listOf(createGlobalFilter()).ifEmpty { null } + videoFeedChannel.typedFilters = listOfNotNull(createContextualFilter(), createFollowTagsFilter()).ifEmpty { null } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BookmarkListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BookmarkListEvent.kt index 822f12d5b..71f1ad7e6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BookmarkListEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BookmarkListEvent.kt @@ -1,9 +1,6 @@ package com.vitorpamplona.amethyst.service.model -import android.util.Log -import com.google.gson.reflect.TypeToken import com.vitorpamplona.amethyst.model.HexKey -import com.vitorpamplona.amethyst.model.toByteArray import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date @@ -15,50 +12,7 @@ class BookmarkListEvent( tags: List>, content: String, sig: HexKey -) : Event(id, pubKey, createdAt, kind, tags, content, sig) { - fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" - fun address() = ATag(kind, pubKey, dTag(), null) - - fun category() = dTag() - fun bookmarkedPosts() = tags.filter { it[0] == "e" }.mapNotNull { it.getOrNull(1) } - - fun plainContent(privKey: ByteArray): String? { - return try { - val sharedSecret = Utils.getSharedSecret(privKey, pubKey.toByteArray()) - - return Utils.decrypt(content, sharedSecret) - } catch (e: Exception) { - Log.w("BookmarkList", "Error decrypting the message ${e.message}") - null - } - } - - @Transient - private var privateTagsCache: List>? = null - - fun privateTags(privKey: ByteArray): List>? { - if (privateTagsCache != null) { - return privateTagsCache - } - - privateTagsCache = try { - gson.fromJson(plainContent(privKey), object : TypeToken>>() {}.type) - } catch (e: Throwable) { - Log.w("BookmarkList", "Error parsing the JSON ${e.message}") - null - } - return privateTagsCache - } - - fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "p" }?.mapNotNull { it.getOrNull(1) } - fun privateTaggedEvents(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "e" }?.mapNotNull { it.getOrNull(1) } - fun privateTaggedAddresses(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "a" }?.mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) - - if (aTagValue != null) ATag.parse(aTagValue, relay) else null - } - +) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) { companion object { const val kind = 30001 @@ -77,24 +31,7 @@ class BookmarkListEvent( createdAt: Long = Date().time / 1000 ): BookmarkListEvent { val pubKey = Utils.pubkeyCreate(privateKey) - - val privTags = mutableListOf>() - privEvents?.forEach { - privTags.add(listOf("e", it)) - } - privUsers?.forEach { - privTags.add(listOf("p", it)) - } - privAddresses?.forEach { - privTags.add(listOf("a", it.toTag())) - } - val msg = gson.toJson(privTags) - - val content = Utils.encrypt( - msg, - privateKey, - pubKey - ) + val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey) val tags = mutableListOf>() tags.add(listOf("d", name)) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt index 3ecf62c9f..0be651a7d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt @@ -238,6 +238,7 @@ open class Event( LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig) MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig) + PeopleListEvent.kind -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig) PollNoteEvent.kind -> PollNoteEvent(id, pubKey, createdAt, tags, content, sig) PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig) ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/GeneralListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/GeneralListEvent.kt new file mode 100644 index 000000000..dd236c96c --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/GeneralListEvent.kt @@ -0,0 +1,90 @@ +package com.vitorpamplona.amethyst.service.model + +import android.util.Log +import com.google.gson.reflect.TypeToken +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toByteArray +import nostr.postr.Utils + +abstract class GeneralListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + kind: Int, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { + fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" + fun address() = ATag(kind, pubKey, dTag(), null) + + fun category() = dTag() + fun bookmarkedPosts() = taggedEvents() + fun bookmarkedPeople() = taggedUsers() + + fun plainContent(privKey: ByteArray): String? { + return try { + val sharedSecret = Utils.getSharedSecret(privKey, pubKey.toByteArray()) + + return Utils.decrypt(content, sharedSecret) + } catch (e: Exception) { + Log.w("GeneralList", "Error decrypting the message ${e.message}") + null + } + } + + @Transient + private var privateTagsCache: List>? = null + + fun privateTags(privKey: ByteArray): List>? { + if (privateTagsCache != null) { + return privateTagsCache + } + + privateTagsCache = try { + gson.fromJson(plainContent(privKey), object : TypeToken>>() {}.type) + } catch (e: Throwable) { + Log.w("GeneralList", "Error parsing the JSON ${e.message}") + null + } + return privateTagsCache + } + + fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] } + fun privateTaggedEvents(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "e" }?.map { it[1] } + fun privateTaggedAddresses(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "a" }?.mapNotNull { + val aTagValue = it.getOrNull(1) + val relay = it.getOrNull(2) + + if (aTagValue != null) ATag.parse(aTagValue, relay) else null + } + + companion object { + fun createPrivateTags( + privEvents: List? = null, + privUsers: List? = null, + privAddresses: List? = null, + + privateKey: ByteArray, + pubKey: ByteArray + ): String { + val privTags = mutableListOf>() + privEvents?.forEach { + privTags.add(listOf("e", it)) + } + privUsers?.forEach { + privTags.add(listOf("p", it)) + } + privAddresses?.forEach { + privTags.add(listOf("a", it.toTag())) + } + val msg = gson.toJson(privTags) + + return Utils.encrypt( + msg, + privateKey, + pubKey + ) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PeopleListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PeopleListEvent.kt new file mode 100644 index 000000000..583f534b5 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PeopleListEvent.kt @@ -0,0 +1,54 @@ +package com.vitorpamplona.amethyst.service.model + +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import nostr.postr.Utils +import java.util.Date + +class PeopleListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) { + companion object { + const val kind = 30000 + + fun create( + name: String = "", + + events: List? = null, + users: List? = null, + addresses: List? = null, + + privEvents: List? = null, + privUsers: List? = null, + privAddresses: List? = null, + + privateKey: ByteArray, + createdAt: Long = Date().time / 1000 + ): PeopleListEvent { + val pubKey = Utils.pubkeyCreate(privateKey) + val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey) + + val tags = mutableListOf>() + tags.add(listOf("d", name)) + + events?.forEach { + tags.add(listOf("e", it)) + } + users?.forEach { + tags.add(listOf("p", it)) + } + addresses?.forEach { + tags.add(listOf("a", it.toTag())) + } + + val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return PeopleListEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey()) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt index 7dd5bb23f..766056a87 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt @@ -1,14 +1,20 @@ package com.vitorpamplona.amethyst.ui.actions import android.content.ContentResolver +import android.content.Context import android.net.Uri +import android.provider.OpenableColumns +import android.util.Log import android.webkit.MimeTypeMap import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vitorpamplona.amethyst.BuildConfig import okhttp3.* import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.asRequestBody import okio.BufferedSink import okio.source +import java.io.File +import java.io.FileOutputStream import java.io.IOException import java.io.InputStream @@ -21,68 +27,88 @@ object ImageUploader { fun uploadImage( uri: Uri, server: ServersAvailable, + context: Context, contentResolver: ContentResolver, onSuccess: (String, String?) -> Unit, onError: (Throwable) -> Unit ) { val contentType = contentResolver.getType(uri) val imageInputStream = contentResolver.openInputStream(uri) - checkNotNull(imageInputStream) { "Can't open the image input stream" } - val myServer = if (server == ServersAvailable.IMGUR) { ImgurServer() } else if (server == ServersAvailable.NOSTRIMG) { NostrImgServer() + } else if (server == ServersAvailable.NOSTR_BUILD) { + NostrBuildServer() } else { ImgurServer() } - uploadImage(imageInputStream, contentType, myServer, onSuccess, onError) + val file = getRealPathFromURI(uri, context)?.let { File(it) } // create path from uri + if (file != null) { + uploadImage(file, imageInputStream, contentType, myServer, server, onSuccess, onError) + } } fun uploadImage( + file: File, inputStream: InputStream, contentType: String?, server: FileServer, + serverType: ServersAvailable, onSuccess: (String, String?) -> Unit, onError: (Throwable) -> Unit ) { val category = contentType?.toMediaType()?.toString()?.split("/")?.get(0) ?: "image" + val fileName = randomChars() val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: "" val client = OkHttpClient.Builder().build() - - val requestBody: RequestBody = MultipartBody.Builder() - .setType(MultipartBody.FORM) - .addFormDataPart( - category, - "$fileName.$extension", - object : RequestBody() { - override fun contentType(): MediaType? = - contentType?.toMediaType() - - override fun writeTo(sink: BufferedSink) { - inputStream.source().use(sink::writeAll) - } - } - ) - .build() - + val requestBody: RequestBody val requestBuilder = Request.Builder() + if (serverType == ServersAvailable.NOSTR_BUILD) { + requestBuilder.addHeader("Content-Type", "multipart/form-data") + requestBody = MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart( + "fileToUpload", + "$fileName.$extension", + file.asRequestBody(contentType?.toMediaType()) + + ) + .build() + } else { + requestBody = MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart( + category, + "$fileName.$extension", + + object : RequestBody() { + override fun contentType(): MediaType? = + contentType?.toMediaType() + + override fun writeTo(sink: BufferedSink) { + inputStream.source().use(sink::writeAll) + } + } + ) + .build() + } + server.clientID()?.let { - requestBuilder.header("Authorization", it) + requestBuilder.addHeader("Authorization", it) } requestBuilder - .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") + .addHeader("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .url(server.postUrl(contentType)) .post(requestBody) - val request = requestBuilder.build() client.newCall(request).enqueue(object : Callback { @@ -111,6 +137,38 @@ object ImageUploader { } } +fun getRealPathFromURI(uri: Uri, context: Context): String? { + val returnCursor = context.contentResolver.query(uri, null, null, null, null) + val nameIndex = returnCursor!!.getColumnIndex(OpenableColumns.DISPLAY_NAME) + returnCursor.moveToFirst() + val name = returnCursor.getString(nameIndex) + val file = File(context.filesDir, name) + try { + val inputStream: InputStream? = context.contentResolver.openInputStream(uri) + val outputStream = FileOutputStream(file) + var read = 0 + val maxBufferSize = 1 * 1024 * 1024 + val bytesAvailable: Int = inputStream?.available() ?: 0 + val bufferSize = Math.min(bytesAvailable, maxBufferSize) + val buffers = ByteArray(bufferSize) + while (inputStream?.read(buffers).also { + if (it != null) { + read = it + } + } != -1 + ) { + outputStream.write(buffers, 0, read) + } + Log.e("File Size", "Size " + file.length()) + inputStream?.close() + outputStream.close() + Log.e("File Path", "Path " + file.path) + } catch (e: java.lang.Exception) { + Log.e("Exception", e.message!!) + } + return file.path +} + abstract class FileServer { abstract fun postUrl(contentType: String?): String abstract fun parseUrlFromSucess(body: String): String? @@ -146,12 +204,10 @@ class ImgurServer : FileServer() { } class NostrBuildServer : FileServer() { - override fun postUrl(contentType: String?) = "https://nostr.build/api/upload/amethyst.php" - + override fun postUrl(contentType: String?) = "https://nostr.build/api/upload/android.php" override fun parseUrlFromSucess(body: String): String? { - val tree = jacksonObjectMapper().readTree(body) - val url = tree?.get("data")?.get("link")?.asText() - return url + val url = jacksonObjectMapper().readTree(body) // return url.toString() + return url.toString().replace("\"", "") } override fun clientID() = null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index 880192257..ac812b2c2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -44,6 +44,8 @@ open class NewMediaModel : ViewModel() { selectedServer = ServersAvailable.IMGUR_NIP_94 } else if (selectedServer == ServersAvailable.NOSTRIMG) { selectedServer = ServersAvailable.NOSTRIMG_NIP_94 + } else if (selectedServer == ServersAvailable.NOSTR_BUILD) { + selectedServer = ServersAvailable.NOSTR_BUILD_NIP_94 } } @@ -75,6 +77,7 @@ open class NewMediaModel : ViewModel() { ImageUploader.uploadImage( uri = uri, server = serverToUse, + context = context, contentResolver = contentResolver, onSuccess = { imageUrl, mimeType -> createNIP94Record(imageUrl, mimeType, description) @@ -135,7 +138,7 @@ open class NewMediaModel : ViewModel() { onReady = { uploadingPercentage.value = 0.90f uploadingDescription.value = "Sending" - val note = account?.sendHeader(it) + account?.sendHeader(it) uploadingPercentage.value = 1.00f isUploadingImage = false onceUploaded() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index 477ca586d..c1489f126 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -116,6 +116,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, acc: Account) { val fileServers = listOf( Triple(ServersAvailable.IMGUR_NIP_94, stringResource(id = R.string.upload_server_imgur_nip94), stringResource(id = R.string.upload_server_imgur_nip94_explainer)), Triple(ServersAvailable.NOSTRIMG_NIP_94, stringResource(id = R.string.upload_server_nostrimg_nip94), stringResource(id = R.string.upload_server_nostrimg_nip94_explainer)), + Triple(ServersAvailable.NOSTR_BUILD_NIP_94, stringResource(id = R.string.upload_server_nostrbuild_nip94), stringResource(id = R.string.upload_server_nostrbuild_nip94_explainer)), Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer)) ) @@ -192,6 +193,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, acc: Account) { if (postViewModel.selectedServer == ServersAvailable.NOSTRIMG_NIP_94 || postViewModel.selectedServer == ServersAvailable.IMGUR_NIP_94 || + postViewModel.selectedServer == ServersAvailable.NOSTR_BUILD_NIP_94 || postViewModel.selectedServer == ServersAvailable.NIP95 ) { Row( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index e6fe1e9a4..370bf8728 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -590,6 +590,7 @@ enum class ServersAvailable { NOSTRIMG, IMGUR_NIP_94, NOSTRIMG_NIP_94, + NOSTR_BUILD_NIP_94, NIP95 } @@ -610,8 +611,10 @@ fun ImageVideoDescription( val fileServers = listOf( Triple(ServersAvailable.IMGUR, stringResource(id = R.string.upload_server_imgur), stringResource(id = R.string.upload_server_imgur_explainer)), Triple(ServersAvailable.NOSTRIMG, stringResource(id = R.string.upload_server_nostrimg), stringResource(id = R.string.upload_server_nostrimg_explainer)), + Triple(ServersAvailable.NOSTR_BUILD, stringResource(id = R.string.upload_server_nostrbuild), stringResource(id = R.string.upload_server_nostrbuild_explainer)), Triple(ServersAvailable.IMGUR_NIP_94, stringResource(id = R.string.upload_server_imgur_nip94), stringResource(id = R.string.upload_server_imgur_nip94_explainer)), Triple(ServersAvailable.NOSTRIMG_NIP_94, stringResource(id = R.string.upload_server_nostrimg_nip94), stringResource(id = R.string.upload_server_nostrimg_nip94_explainer)), + Triple(ServersAvailable.NOSTR_BUILD_NIP_94, stringResource(id = R.string.upload_server_nostrbuild_nip94), stringResource(id = R.string.upload_server_nostrbuild_nip94_explainer)), Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer)) ) @@ -741,6 +744,7 @@ fun ImageVideoDescription( if (selectedServer == ServersAvailable.NOSTRIMG_NIP_94 || selectedServer == ServersAvailable.IMGUR_NIP_94 || + selectedServer == ServersAvailable.NOSTR_BUILD_NIP_94 || selectedServer == ServersAvailable.NIP95 ) { Row( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index fd38e3031..42590becc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -145,9 +145,10 @@ open class NewPostViewModel : ViewModel() { ImageUploader.uploadImage( uri = it, server = server, + context = context, contentResolver = contentResolver, onSuccess = { imageUrl, mimeType -> - if (server == ServersAvailable.IMGUR_NIP_94 || server == ServersAvailable.NOSTRIMG_NIP_94) { + if (server == ServersAvailable.IMGUR_NIP_94 || server == ServersAvailable.NOSTRIMG_NIP_94 || server == ServersAvailable.NOSTR_BUILD_NIP_94) { createNIP94Record(imageUrl, mimeType, description) } else { isUploadingImage = false diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index 269f22871..80abdf2cf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -171,6 +171,7 @@ class NewUserMetadataViewModel : ViewModel() { ImageUploader.uploadImage( uri = it, server = account.defaultFileServer, + context = context, contentResolver = context.contentResolver, onSuccess = { imageUrl, mimeType -> onUploading(false) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AsyncImageProxy.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AsyncImageProxy.kt index 8e4dbcd33..4d89a4dfd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AsyncImageProxy.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AsyncImageProxy.kt @@ -22,7 +22,7 @@ data class ResizeImage(val url: String?, val size: Dp) { val imgPx = 200 // with(LocalDensity.current) { model.size.toPx().toInt() } val base64 = Base64.getUrlEncoder().encodeToString(url.toByteArray()) - return "https://d12fidohs5rlxk.cloudfront.net/preset:sharp/rs:fit:$imgPx:$imgPx:0/gravity:sm/$base64" + return url // "https://d12fidohs5rlxk.cloudfront.net/preset:sharp/rs:fit:$imgPx:$imgPx:0/gravity:sm/$base64" } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt index 7a4b3ca16..40f40f839 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt @@ -29,10 +29,18 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog @Composable -fun TextSpinner(label: String, placeholder: String, options: List, explainers: List? = null, onSelect: (Int) -> Unit, modifier: Modifier = Modifier) { +fun TextSpinner( + label: String, + placeholder: String, + options: List, + explainers: List? = null, + onSelect: (Int) -> Unit, + modifier: Modifier = Modifier +) { val focusRequester = remember { FocusRequester() } val interactionSource = remember { MutableInteractionSource() } var optionsShowing by remember { mutableStateOf(false) } @@ -106,7 +114,7 @@ fun SpinnerSelectionDialog(options: List, explainers: List?, onD modifier = Modifier .fillMaxWidth() ) { - Text(text = it, color = Color.Gray) + Text(text = it, color = Color.Gray, fontSize = 14.sp) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 3f522c35c..c2139a7f0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -3,9 +3,6 @@ package com.vitorpamplona.amethyst.ui.components import android.content.Context import android.util.Log import android.widget.Toast -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeOut import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -13,6 +10,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio @@ -57,6 +55,7 @@ import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.isFinite import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties @@ -173,11 +172,6 @@ fun ZoomableContentView(content: ZoomableContent, images: List RoundedCornerShape(15.dp) ) - val ratio = aspectRatio(content.dim) - if (ratio != null) { - mainImageModifier = mainImageModifier.aspectRatio(ratio) - } - if (content is ZoomableUrlContent) { mainImageModifier = mainImageModifier.combinedClickable( onClick = { dialogOpen = true }, @@ -216,13 +210,22 @@ private fun LocalImageView( mutableStateOf(null) } - Box(contentAlignment = Alignment.Center) { + val ratio = aspectRatio(content.dim) + + BoxWithConstraints(contentAlignment = Alignment.Center) { + val myModifier = mainImageModifier.also { + if (ratio != null) { + it.aspectRatio(ratio, maxHeight.isFinite) + } + } + val contentScale = if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth + if (content.localFile.exists()) { AsyncImage( model = content.localFile, contentDescription = content.description, - contentScale = ContentScale.FillWidth, - modifier = mainImageModifier, + contentScale = contentScale, + modifier = myModifier, onLoading = { imageState = it }, @@ -234,14 +237,9 @@ private fun LocalImageView( if (imageState is AsyncImagePainter.State.Success) { HashVerificationSymbol(content.isVerified, Modifier.align(Alignment.TopEnd)) - } - - AnimatedVisibility( - visible = imageState !is AsyncImagePainter.State.Success, - exit = fadeOut(animationSpec = tween(200)) - ) { + } else { if (content.blurhash != null) { - DisplayBlurHash(content.blurhash, content.description, mainImageModifier) + DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier) } else { FlowRow() { DisplayUrlWithLoadingSymbol(content) @@ -272,6 +270,8 @@ private fun UrlImageView( mutableStateOf(null) } + val ratio = aspectRatio(content.dim) + LaunchedEffect(key1 = content.url, key2 = imageState) { if (imageState is AsyncImagePainter.State.Success) { scope.launch(Dispatchers.IO) { @@ -280,12 +280,19 @@ private fun UrlImageView( } } - Box(contentAlignment = Alignment.Center) { + BoxWithConstraints(contentAlignment = Alignment.Center) { + val myModifier = mainImageModifier.also { + if (ratio != null) { + it.aspectRatio(ratio, maxHeight.isFinite) + } + } + val contentScale = if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth + AsyncImage( model = content.url, contentDescription = content.description, - contentScale = ContentScale.FillWidth, - modifier = mainImageModifier, + contentScale = contentScale, + modifier = myModifier, onLoading = { imageState = it }, @@ -296,14 +303,9 @@ private fun UrlImageView( if (imageState is AsyncImagePainter.State.Success) { HashVerificationSymbol(verifiedHash, Modifier.align(Alignment.TopEnd)) - } - - AnimatedVisibility( - visible = imageState !is AsyncImagePainter.State.Success, - exit = fadeOut(animationSpec = tween(200)) - ) { + } else { if (content.blurhash != null) { - DisplayBlurHash(content.blurhash, content.description, mainImageModifier) + DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier) } else { FlowRow() { DisplayUrlWithLoadingSymbol(content) @@ -389,6 +391,7 @@ private fun DisplayUrlWithLoadingSymbolWait(content: ZoomableContent) { private fun DisplayBlurHash( blurhash: String?, description: String?, + contentScale: ContentScale, modifier: Modifier ) { if (blurhash == null) return @@ -400,7 +403,7 @@ private fun DisplayBlurHash( blurhash ), contentDescription = description, - contentScale = ContentScale.FillWidth, + contentScale = contentScale, modifier = modifier ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt index 6026e2669..a5cda41b9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt @@ -18,9 +18,8 @@ object HomeConversationsFeedFilter : AdditiveFeedFilter() { } private fun innerApplyFilter(collection: Collection): Set { - val user = account.userProfile() - val followingKeySet = user.cachedFollowingKeySet() - val followingTagSet = user.cachedFollowingTagSet() + val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet() + val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet() return collection .asSequence() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt index b5f62086e..fcd175020 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt @@ -24,9 +24,8 @@ object HomeNewThreadFeedFilter : AdditiveFeedFilter() { } private fun innerApplyFilter(collection: Collection): Set { - val user = account.userProfile() - val followingKeySet = user.cachedFollowingKeySet() - val followingTagSet = user.cachedFollowingTagSet() + val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet() + val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet() return collection .asSequence() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/PeopleListFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/PeopleListFeedFilter.kt new file mode 100644 index 000000000..11263ffc8 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/PeopleListFeedFilter.kt @@ -0,0 +1,25 @@ +package com.vitorpamplona.amethyst.ui.dal + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.model.PeopleListEvent + +object PeopleListFeedFilter : FeedFilter() { + lateinit var account: Account + + override fun feed(): List { + val privKey = account.loggedIn.privKey ?: return emptyList() + + val lists = LocalCache.addressables.values + .asSequence() + .filter { + (it.event is PeopleListEvent) + } + .toSet() + + return lists + .sortedBy { it.createdAt() } + .reversed() + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt index 6ab0901cb..be17bf33a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.* @@ -20,17 +21,17 @@ object VideoFeedFilter : AdditiveFeedFilter() { private fun innerApplyFilter(collection: Collection): Set { val now = System.currentTimeMillis() / 1000 + val isGlobal = account.defaultStoriesFollowList == GLOBAL_FOLLOWS + + val followingKeySet = account.selectedUsersFollowList(account.defaultStoriesFollowList) ?: emptySet() + val followingTagSet = account.selectedTagsFollowList(account.defaultStoriesFollowList) ?: emptySet() return collection .asSequence() - .filter { - it.event is FileHeaderEvent || it.event is FileStorageHeaderEvent - } + .filter { it.event is FileHeaderEvent || it.event is FileStorageHeaderEvent } + .filter { isGlobal || it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false) } .filter { account.isAcceptable(it) } - .filter { - // Do not show notes with the creation time exceeding the current time, as they will always stay at the top of the global feed, which is cheating. - it.createdAt()!! <= now - } + .filter { it.createdAt()!! <= now } .toSet() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index c99cfe66a..4415e4648 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.navigation import android.util.Log import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -23,6 +24,8 @@ import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -41,6 +44,9 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import coil.Coil import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS +import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.NostrAccountDataSource import com.vitorpamplona.amethyst.service.NostrChannelDataSource @@ -55,6 +61,7 @@ import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource import com.vitorpamplona.amethyst.service.NostrThreadDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource +import com.vitorpamplona.amethyst.service.model.PeopleListEvent import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.ui.actions.NewRelayListView @@ -62,33 +69,58 @@ import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @Composable fun AppTopBar(navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) { - when (currentRoute(navController)) { + when (currentRoute(navController)?.substringBefore("?")) { // Route.Profile.route -> TopBarWithBackButton(navController) + Route.Home.base -> HomeTopBar(scaffoldState, accountViewModel) + Route.Video.base -> StoriesTopBar(scaffoldState, accountViewModel) else -> MainTopBar(scaffoldState, accountViewModel) } } +@Composable +fun StoriesTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) { + GenericTopBar(scaffoldState, accountViewModel) { account -> + FollowList(account.defaultStoriesFollowList, true) { listName -> + account.changeDefaultStoriesFollowList(listName) + } + } +} + +@Composable +fun HomeTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) { + GenericTopBar(scaffoldState, accountViewModel) { account -> + FollowList(account.defaultHomeFollowList, false) { listName -> + account.changeDefaultHomeFollowList(listName) + } + } +} + +@Composable +fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) { + GenericTopBar(scaffoldState, accountViewModel) { + AmethystIcon() + } +} + @OptIn(coil.annotation.ExperimentalCoilApi::class) @Composable -fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) { +fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, content: @Composable (Account) -> Unit) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return - val accountUserState by account.userProfile().live().metadata.observeAsState() - val accountUser = accountUserState?.user ?: return - val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() } val connectedRelaysLiveData by relayViewModel.connectedRelaysLiveData.observeAsState() val availableRelaysLiveData by relayViewModel.availableRelaysLiveData.observeAsState() val coroutineScope = rememberCoroutineScope() - val context = LocalContext.current - var wantsToEditRelays by remember { mutableStateOf(false) } @@ -115,48 +147,7 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - IconButton( - onClick = { - Client.allSubscriptions().map { - "$it ${ - Client.getSubscriptionFilters(it) - .joinToString { it.filter.toJson() } - }" - }.forEach { - Log.d("STATE DUMP", it) - } - - NostrAccountDataSource.printCounter() - NostrChannelDataSource.printCounter() - NostrChatroomDataSource.printCounter() - NostrChatroomListDataSource.printCounter() - NostrGlobalDataSource.printCounter() - NostrHashtagDataSource.printCounter() - NostrHomeDataSource.printCounter() - NostrSearchEventOrUserDataSource.printCounter() - NostrSingleChannelDataSource.printCounter() - NostrSingleEventDataSource.printCounter() - NostrSingleUserDataSource.printCounter() - NostrThreadDataSource.printCounter() - NostrUserProfileDataSource.printCounter() - - Log.d("STATE DUMP", "Connected Relays: " + RelayPool.connectedRelays()) - - val imageLoader = Coil.imageLoader(context) - Log.d("STATE DUMP", "Image Disk Cache ${(imageLoader.diskCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.diskCache?.maxSize ?: 0) / (1024 * 1024)} MB") - Log.d("STATE DUMP", "Image Memory Cache ${(imageLoader.memoryCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.memoryCache?.maxSize ?: 0) / (1024 * 1024)} MB") - - Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.event != null }.size + "/" + LocalCache.notes.size) - Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + "/" + LocalCache.users.size) - } - ) { - Icon( - painter = painterResource(R.drawable.amethyst), - null, - modifier = Modifier.size(40.dp), - tint = Color.Unspecified - ) - } + content(account) } Column( @@ -186,23 +177,10 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) } }, navigationIcon = { - IconButton( - onClick = { - coroutineScope.launch { - scaffoldState.drawerState.open() - } - }, - modifier = Modifier - ) { - RobohashAsyncImageProxy( - robot = accountUser.pubkeyHex, - model = ResizeImage(accountUser.profilePicture(), 34.dp), - contentDescription = stringResource(id = R.string.profile_image), - modifier = Modifier - .width(34.dp) - .height(34.dp) - .clip(shape = CircleShape) - ) + LoggedInUserPictureDrawer(account) { + coroutineScope.launch { + scaffoldState.drawerState.open() + } } }, actions = { @@ -223,6 +201,107 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) } } +@Composable +private fun LoggedInUserPictureDrawer( + account: Account, + onClick: () -> Unit +) { + val accountUserState by account.userProfile().live().metadata.observeAsState() + val accountUser = accountUserState?.user ?: return + + IconButton( + onClick = onClick, + modifier = Modifier + ) { + RobohashAsyncImageProxy( + robot = accountUser.pubkeyHex, + model = ResizeImage(accountUser.profilePicture(), 34.dp), + contentDescription = stringResource(id = R.string.profile_image), + modifier = Modifier + .width(34.dp) + .height(34.dp) + .clip(shape = CircleShape) + ) + } +} + +@Composable +fun FollowList(listName: String, withGlobal: Boolean, onChange: (String) -> Unit) { + // Notification + val dbState = LocalCache.live.observeAsState() + val db = dbState.value ?: return + + val kind3Follow = Pair(KIND3_FOLLOWS, stringResource(id = R.string.follow_list_kind3follows)) + val globalFollow = Pair(GLOBAL_FOLLOWS, stringResource(id = R.string.follow_list_global)) + + val defaultOptions = if (withGlobal) listOf(kind3Follow, globalFollow) else listOf(kind3Follow) + + var followLists by remember { mutableStateOf(defaultOptions) } + val followNames = remember { derivedStateOf { followLists.map { it.second } } } + + LaunchedEffect(key1 = db) { + withContext(Dispatchers.IO) { + followLists = defaultOptions + LocalCache.addressables.mapNotNull { + val event = (it.value.event as? PeopleListEvent) + // Has to have an list + if (event != null && (event.tags.size > 1 || event.content.length > 50)) { + Pair(event.dTag(), event.dTag()) + } else { + null + } + }.sortedBy { it.second } + } + } + + SimpleTextSpinner( + placeholder = followLists.firstOrNull { it.first == listName }?.first ?: KIND3_FOLLOWS, + options = followNames.value, + onSelect = { + onChange(followLists.getOrNull(it)?.first ?: KIND3_FOLLOWS) + } + ) +} + +@Composable +fun SimpleTextSpinner( + placeholder: String, + options: List, + explainers: List? = null, + onSelect: (Int) -> Unit, + modifier: Modifier = Modifier +) { + val interactionSource = remember { MutableInteractionSource() } + var optionsShowing by remember { mutableStateOf(false) } + var currentText by remember { mutableStateOf(placeholder) } + + Box( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + Text(currentText) + Box( + modifier = Modifier + .matchParentSize() + .clickable( + interactionSource = interactionSource, + indication = null + ) { + optionsShowing = true + } + ) + } + + if (optionsShowing) { + options.isNotEmpty().also { + SpinnerSelectionDialog(options = options, explainers = explainers, onDismiss = { optionsShowing = false }) { + currentText = options[it] + optionsShowing = false + onSelect(it) + } + } + } +} + @Composable fun TopBarWithBackButton(navController: NavHostController) { Column() { @@ -250,3 +329,51 @@ fun TopBarWithBackButton(navController: NavHostController) { Divider(thickness = 0.25.dp) } } + +@Composable +fun AmethystIcon() { + val context = LocalContext.current + + IconButton( + onClick = { + Client.allSubscriptions().map { + "$it ${ + Client.getSubscriptionFilters(it) + .joinToString { it.filter.toJson() } + }" + }.forEach { + Log.d("STATE DUMP", it) + } + + NostrAccountDataSource.printCounter() + NostrChannelDataSource.printCounter() + NostrChatroomDataSource.printCounter() + NostrChatroomListDataSource.printCounter() + NostrGlobalDataSource.printCounter() + NostrHashtagDataSource.printCounter() + NostrHomeDataSource.printCounter() + NostrSearchEventOrUserDataSource.printCounter() + NostrSingleChannelDataSource.printCounter() + NostrSingleEventDataSource.printCounter() + NostrSingleUserDataSource.printCounter() + NostrThreadDataSource.printCounter() + NostrUserProfileDataSource.printCounter() + + Log.d("STATE DUMP", "Connected Relays: " + RelayPool.connectedRelays()) + + val imageLoader = Coil.imageLoader(context) + Log.d("STATE DUMP", "Image Disk Cache ${(imageLoader.diskCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.diskCache?.maxSize ?: 0) / (1024 * 1024)} MB") + Log.d("STATE DUMP", "Image Memory Cache ${(imageLoader.memoryCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.memoryCache?.maxSize ?: 0) / (1024 * 1024)} MB") + + Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.event != null }.size + "/" + LocalCache.notes.size) + Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + "/" + LocalCache.users.size) + } + ) { + Icon( + painter = painterResource(R.drawable.amethyst), + null, + modifier = Modifier.size(40.dp), + tint = Color.Unspecified + ) + } +} 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 8d704e699..ad71c37ae 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 @@ -46,6 +46,7 @@ import com.google.accompanist.flowlayout.FlowRow import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -202,113 +203,44 @@ fun NoteComposeInner( } Column( - modifier = modifier - .combinedClickable( - onClick = { - if (noteEvent is ChannelMessageEvent) { - baseChannel?.let { - navController.navigate("Channel/${it.idHex}") - } - } else if (noteEvent is PrivateDmEvent) { - val replyAuthorBase = - (note.event as? PrivateDmEvent) - ?.recipientPubKey() - ?.let { LocalCache.getOrCreateUser(it) } - - var userToComposeOn = note.author!! - - if (replyAuthorBase != null) { - if (note.author == accountViewModel.userProfile()) { - userToComposeOn = replyAuthorBase - } - } - - navController.navigate("Room/${userToComposeOn.pubkeyHex}") - } else { - navController.navigate("Note/${note.idHex}") + modifier = modifier.combinedClickable( + onClick = { + if (noteEvent is ChannelMessageEvent) { + baseChannel?.let { + navController.navigate("Channel/${it.idHex}") } - }, - onLongClick = { popupExpanded = true } - ) - .background(backgroundColor) + } else if (noteEvent is PrivateDmEvent) { + val replyAuthorBase = + (note.event as? PrivateDmEvent) + ?.recipientPubKey() + ?.let { LocalCache.getOrCreateUser(it) } + + var userToComposeOn = note.author!! + + if (replyAuthorBase != null) { + if (note.author == accountViewModel.userProfile()) { + userToComposeOn = replyAuthorBase + } + } + + navController.navigate("Room/${userToComposeOn.pubkeyHex}") + } else { + navController.navigate("Note/${note.idHex}") + } + }, + onLongClick = { popupExpanded = true } + ).background(backgroundColor) ) { Row( modifier = Modifier .padding( start = if (!isBoostedNote) 12.dp else 0.dp, end = if (!isBoostedNote) 12.dp else 0.dp, - top = if (addMarginTop) 10.dp else 0.dp + top = if (addMarginTop && !isBoostedNote) 10.dp else 0.dp ) ) { if (!isBoostedNote && !isQuotedNote) { - Column(Modifier.width(55.dp)) { - // Draws the boosted picture outside the boosted card. - Box( - modifier = Modifier - .width(55.dp) - .padding(0.dp) - ) { - NoteAuthorPicture(note, navController, loggedIn, 55.dp) - - if (noteEvent is RepostEvent) { - note.replyTo?.lastOrNull()?.let { - Box( - Modifier - .width(30.dp) - .height(30.dp) - .align(Alignment.BottomEnd) - ) { - NoteAuthorPicture( - it, - navController, - loggedIn, - 35.dp, - pictureModifier = Modifier.border(2.dp, MaterialTheme.colors.background, CircleShape) - ) - } - } - } - - // boosted picture - if (noteEvent is ChannelMessageEvent && baseChannel != null) { - val channelState by baseChannel.live.observeAsState() - val channel = channelState?.channel - - if (channel != null) { - Box( - Modifier - .width(30.dp) - .height(30.dp) - .align(Alignment.BottomEnd) - ) { - RobohashAsyncImageProxy( - robot = channel.idHex, - model = ResizeImage(channel.profilePicture(), 30.dp), - contentDescription = stringResource(R.string.group_picture), - modifier = Modifier - .width(30.dp) - .height(30.dp) - .clip(shape = CircleShape) - .background(MaterialTheme.colors.background) - .border( - 2.dp, - MaterialTheme.colors.background, - CircleShape - ) - ) - } - } - } - } - - if (noteEvent is RepostEvent) { - baseNote.replyTo?.lastOrNull()?.let { - RelayBadges(it) - } - } else { - RelayBadges(baseNote) - } - } + DrawAuthorImages(baseNote, loggedIn, navController) } Column( @@ -354,7 +286,7 @@ fun NoteComposeInner( } } - if (note.author != null && !makeItShort && !isQuotedNote) { + if (noteEvent !is RepostEvent && note.author != null && !makeItShort && !isQuotedNote) { Row(verticalAlignment = Alignment.CenterVertically) { ObserveDisplayNip05Status(note.author!!, Modifier.weight(1f)) @@ -370,7 +302,7 @@ fun NoteComposeInner( } } - Spacer(modifier = Modifier.height(3.dp)) + Spacer(modifier = Modifier.height(2.dp)) if (!makeItShort && noteEvent is TextNoteEvent && (note.replyTo != null || noteEvent.mentions().isNotEmpty())) { val replyingDirectlyTo = note.replyTo?.lastOrNull() @@ -596,6 +528,91 @@ fun NoteComposeInner( } } +@Composable +private fun DrawAuthorImages(baseNote: Note, loggedIn: User, navController: NavController) { + val baseChannel = baseNote.channel() + + Column(Modifier.width(55.dp)) { + // Draws the boosted picture outside the boosted card. + Box(modifier = Modifier.width(55.dp), contentAlignment = Alignment.BottomEnd) { + NoteAuthorPicture(baseNote, navController, loggedIn, 55.dp) + + if (baseNote.event is RepostEvent) { + RepostNoteAuthorPicture(baseNote, navController, loggedIn) + } + + if (baseNote.event is ChannelMessageEvent && baseChannel != null) { + ChannelNotePicture(baseChannel) + } + } + + if (baseNote.event is RepostEvent) { + baseNote.replyTo?.lastOrNull()?.let { + RelayBadges(it) + } + } else { + RelayBadges(baseNote) + } + } +} + +@Composable +private fun ChannelNotePicture(baseChannel: Channel) { + val channelState by baseChannel.live.observeAsState() + val channel = channelState?.channel + + if (channel != null) { + Box( + Modifier + .width(30.dp) + .height(30.dp) + ) { + RobohashAsyncImageProxy( + robot = channel.idHex, + model = ResizeImage(channel.profilePicture(), 30.dp), + contentDescription = stringResource(R.string.group_picture), + modifier = Modifier + .width(30.dp) + .height(30.dp) + .clip(shape = CircleShape) + .background(MaterialTheme.colors.background) + .border( + 2.dp, + MaterialTheme.colors.background, + CircleShape + ) + ) + } + } +} + +@Composable +private fun RepostNoteAuthorPicture( + baseNote: Note, + navController: NavController, + loggedIn: User +) { + baseNote.replyTo?.lastOrNull()?.let { + Box( + Modifier + .width(30.dp) + .height(30.dp) + ) { + NoteAuthorPicture( + it, + navController, + loggedIn, + 35.dp, + pictureModifier = Modifier.border( + 2.dp, + MaterialTheme.colors.background, + CircleShape + ) + ) + } + } +} + @Composable fun DisplayHighlight( highlight: String, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index af8adb2c3..5d7c6265c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -50,9 +51,11 @@ fun HomeScreen( ) { val coroutineScope = rememberCoroutineScope() val account = accountViewModel.accountLiveData.value?.account ?: return - var wantsToAddNip47 by remember { mutableStateOf(nip47) } + var wantsToAddNip47 by remember { mutableStateOf(nip47) } - LaunchedEffect(accountViewModel) { + val accountState = account.live.observeAsState() + + LaunchedEffect(accountViewModel, accountState.value?.account?.defaultHomeFollowList) { HomeNewThreadFeedFilter.account = account HomeConversationsFeedFilter.account = account NostrHomeDataSource.resetFilters() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt index adba2b7ea..4ae9fef50 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt @@ -105,10 +105,14 @@ fun VideoScreen( val lifeCycleOwner = LocalLifecycleOwner.current val account = accountViewModel.accountLiveData.value?.account ?: return + val accountState = account.live.observeAsState() + + NostrVideoDataSource.account = account VideoFeedFilter.account = account - LaunchedEffect(accountViewModel) { + LaunchedEffect(accountViewModel, accountState.value?.account?.defaultStoriesFollowList) { VideoFeedFilter.account = account + NostrVideoDataSource.account = account NostrVideoDataSource.resetFilters() videoFeedView.invalidateData() } @@ -118,6 +122,7 @@ fun VideoScreen( if (event == Lifecycle.Event.ON_RESUME) { println("Video Start") VideoFeedFilter.account = account + NostrVideoDataSource.account = account NostrVideoDataSource.start() videoFeedView.invalidateData() } @@ -237,7 +242,7 @@ private fun RenderVideoOrPictureNote( var moreActionsExpanded by remember { mutableStateOf(false) } Column(Modifier.fillMaxSize(1f)) { - Row(Modifier.weight(1f, true), verticalAlignment = Alignment.CenterVertically) { + Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) { if (noteEvent is FileHeaderEvent) { FileHeaderDisplay(note) } else if (noteEvent is FileStorageHeaderEvent) { diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index c47fda57e..64568685b 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -333,5 +333,11 @@ Saját csomópontjaid (NIP-95) A fájlokat a csomópontokra töltik fel és ott tárolják. Rögzített URL-től mentesek (harmadik féltől való függőség). Győződj meg róla, hogy legalább egy NIP-95 csomópont a csomópontlistában szerepel + + + + Követek Lista + Mindenki akit követek + Globális diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index b85757a84..e4760f5af 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -280,9 +280,56 @@ "Zoeken naar event %1$s" Een openbaar bericht toevoegen + Een privébericht toevoegen + Een invoice bericht toevoegen Bedankt voor al jullie werk! Maken en toevoegen Poll auteurs kunnen niet stemmen in hun eigen poll. #zappoll + + De afbeelding is onveranderd sinds de post + De afbeelding is veranderd. De auteur heeft de wijziging misschien niet gezien + + Afbeelding toevoegen + Video toevoegen + Document toevoegen + + Voeg toe aan bericht + Beschrijving van de inhoud + Een blauwe boot in een wit zandstrand bij zonsondergang + + Zap type + Zap type voor alle opties + + Publiek + Iedereen kan de transactie en het bericht zien + + Privé + Verzender en ontvanger kunnen elkaar zien en het bericht lezen + + Anoniem + Ontvanger en het publiek weten niet wie de betaling verzond + + Non-Zap + Geen spoor binnen Nostr, alleen in Lightning + + + File server + LnAddress of @User + + imgur.com - minder beveiligd + Upload naar Imgur. Imgur kan uw afbeelding/video op elk moment wijzigen + + nostrimg.com - minder beveiligd + Upload naar NostrImg. NostrImg kan uw afbeelding/video op elk moment wijzigen + + Verifieerbare Imgur (NIP-94) + Beschermt tegen het achteraf wijzigen van de inhoud door Imgur. Dit is een nieuwe NIP: andere clients zien de afbeelding misschien niet. + + Verifieerbare NostrImg (NIP-94) + Beschermt tegen het achteraf wijzigen van de inhoud door NostrImg. Dit is een nieuwe NIP: andere clients zien de afbeelding misschien niet. + + Uw relays (NIP-95) + Bestanden worden geüpload naar en gehost door relays. Ze zijn vrij van een vaste url (afhankelijkheid van derden). Zorg ervoor dat u een NIP-95 relay in uw lijst met relays hebt. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 64fcffb60..143f5b6b1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -327,19 +327,31 @@ File Server LnAddress or @User - imgur.com - less secure - Uploads to Imgur. Imgur can change your image/video at any time + imgur.com - trusted + Imgur can modify the file - nostrimg.com - less secure - Uploads to NostrImg. NostrImg can change your image at any time + nostrimg.com - trusted + NostrImg can modify the file + + nostr.build - trusted + Nostr.build can modify the file Verifiable Imgur (NIP-94) - Protects from Imgur modifying the content afterwards. This is a new NIP: other clients might not see the image + Checks if Imgur modified the file. New NIP: other clients might not see it Verifiable NostrImg (NIP-94) - Protects from NostrImg modifying the content afterwards. This is a new NIP: other clients might not see the image + Checks if NostrImg modified the file. New NIP: other clients might not see it + + Verifiable Nostr.build (NIP-94) + Checks if Nostr.build modified the file. New NIP: other clients might not see it Your relays (NIP-95) - Files are uploaded to and hosted by relays. They are free from a fixed url (third-party dependency). Make sure to have a NIP-95 relay in your relay list + Files are hosted by your relays. New NIP: check if they support + + + + Follow List + All Follows + Global