Merge remote-tracking branch 'upstream/main'
This commit is contained in:
+2
-2
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId "com.vitorpamplona.amethyst"
|
applicationId "com.vitorpamplona.amethyst"
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 140
|
versionCode 144
|
||||||
versionName "0.40.6"
|
versionName "0.42.2"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import android.content.SharedPreferences
|
|||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
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.RelaySetupInfo
|
||||||
import com.vitorpamplona.amethyst.model.toByteArray
|
import com.vitorpamplona.amethyst.model.toByteArray
|
||||||
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
||||||
@@ -46,6 +48,8 @@ private object PrefKeys {
|
|||||||
const val ZAP_AMOUNTS = "zapAmounts"
|
const val ZAP_AMOUNTS = "zapAmounts"
|
||||||
const val DEFAULT_ZAPTYPE = "defaultZapType"
|
const val DEFAULT_ZAPTYPE = "defaultZapType"
|
||||||
const val DEFAULT_FILE_SERVER = "defaultFileServer"
|
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 ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer"
|
||||||
const val LATEST_CONTACT_LIST = "latestContactList"
|
const val LATEST_CONTACT_LIST = "latestContactList"
|
||||||
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
|
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.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices))
|
||||||
putString(PrefKeys.DEFAULT_ZAPTYPE, gson.toJson(account.defaultZapType))
|
putString(PrefKeys.DEFAULT_ZAPTYPE, gson.toJson(account.defaultZapType))
|
||||||
putString(PrefKeys.DEFAULT_FILE_SERVER, gson.toJson(account.defaultFileServer))
|
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.ZAP_PAYMENT_REQUEST_SERVER, gson.toJson(account.zapPaymentRequest))
|
||||||
putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList))
|
putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList))
|
||||||
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
|
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
|
||||||
@@ -217,6 +223,8 @@ object LocalPreferences {
|
|||||||
|
|
||||||
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
|
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
|
||||||
val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language
|
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(
|
val zapAmountChoices = gson.fromJson(
|
||||||
getString(PrefKeys.ZAP_AMOUNTS, "[]"),
|
getString(PrefKeys.ZAP_AMOUNTS, "[]"),
|
||||||
@@ -278,6 +286,8 @@ object LocalPreferences {
|
|||||||
zapAmountChoices,
|
zapAmountChoices,
|
||||||
defaultZapType,
|
defaultZapType,
|
||||||
defaultFileServer,
|
defaultFileServer,
|
||||||
|
defaultHomeFollowList,
|
||||||
|
defaultStoriesFollowList,
|
||||||
zapPaymentRequestServer,
|
zapPaymentRequestServer,
|
||||||
hideDeleteRequestDialog,
|
hideDeleteRequestDialog,
|
||||||
hideBlockAlertDialog,
|
hideBlockAlertDialog,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
|||||||
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
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.Client
|
||||||
import com.vitorpamplona.amethyst.service.relays.Constants
|
import com.vitorpamplona.amethyst.service.relays.Constants
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ object ServiceManager {
|
|||||||
NostrAccountDataSource.account = myAccount
|
NostrAccountDataSource.account = myAccount
|
||||||
NostrHomeDataSource.account = myAccount
|
NostrHomeDataSource.account = myAccount
|
||||||
NostrChatroomListDataSource.account = myAccount
|
NostrChatroomListDataSource.account = myAccount
|
||||||
|
NostrVideoDataSource.account = myAccount
|
||||||
|
|
||||||
// Notification Elements
|
// Notification Elements
|
||||||
NostrHomeDataSource.start()
|
NostrHomeDataSource.start()
|
||||||
@@ -61,6 +63,7 @@ object ServiceManager {
|
|||||||
NostrSingleUserDataSource.stop()
|
NostrSingleUserDataSource.stop()
|
||||||
NostrThreadDataSource.stop()
|
NostrThreadDataSource.stop()
|
||||||
NostrUserProfileDataSource.stop()
|
NostrUserProfileDataSource.stop()
|
||||||
|
NostrVideoDataSource.stop()
|
||||||
|
|
||||||
Client.disconnect()
|
Client.disconnect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ fun getLanguagesSpokenByUser(): Set<String> {
|
|||||||
return codedList
|
return codedList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val GLOBAL_FOLLOWS = " Global "
|
||||||
|
val KIND3_FOLLOWS = " All Follows "
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
class Account(
|
class Account(
|
||||||
val loggedIn: Persona,
|
val loggedIn: Persona,
|
||||||
@@ -50,6 +53,8 @@ class Account(
|
|||||||
var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L),
|
var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L),
|
||||||
var defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PRIVATE,
|
var defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PRIVATE,
|
||||||
var defaultFileServer: ServersAvailable = ServersAvailable.IMGUR,
|
var defaultFileServer: ServersAvailable = ServersAvailable.IMGUR,
|
||||||
|
var defaultHomeFollowList: String = KIND3_FOLLOWS,
|
||||||
|
var defaultStoriesFollowList: String = GLOBAL_FOLLOWS,
|
||||||
var zapPaymentRequest: Nip47URI? = null,
|
var zapPaymentRequest: Nip47URI? = null,
|
||||||
var hideDeleteRequestDialog: Boolean = false,
|
var hideDeleteRequestDialog: Boolean = false,
|
||||||
var hideBlockAlertDialog: Boolean = false,
|
var hideBlockAlertDialog: Boolean = false,
|
||||||
@@ -403,12 +408,12 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createNip95(data: ByteArray, headerInfo: FileHeader): Pair<FileStorageEvent, FileStorageHeaderEvent>? {
|
fun createNip95(byteArray: ByteArray, headerInfo: FileHeader): Pair<FileStorageEvent, FileStorageHeaderEvent>? {
|
||||||
if (!isWriteable()) return null
|
if (!isWriteable()) return null
|
||||||
|
|
||||||
val data = FileStorageEvent.create(
|
val data = FileStorageEvent.create(
|
||||||
mimeType = headerInfo.mimeType ?: "",
|
mimeType = headerInfo.mimeType ?: "",
|
||||||
data = data,
|
data = byteArray,
|
||||||
privateKey = loggedIn.privKey!!
|
privateKey = loggedIn.privKey!!
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -729,6 +734,18 @@ class Account(
|
|||||||
saveable.invalidateData()
|
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<Long>) {
|
fun changeZapAmounts(newAmounts: List<Long>) {
|
||||||
zapAmountChoices = newAmounts
|
zapAmountChoices = newAmounts
|
||||||
live.invalidateData()
|
live.invalidateData()
|
||||||
@@ -741,6 +758,37 @@ class Account(
|
|||||||
saveable.invalidateData()
|
saveable.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun selectedUsersFollowList(listName: String?): Set<String>? {
|
||||||
|
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<String>? {
|
||||||
|
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) {
|
fun sendChangeChannel(name: String, about: String, picture: String, channel: Channel) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
"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))
|
"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))
|
"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))
|
"footstr" -> HashtagIcon(R.drawable.footstr, "Footstr", Color.Unspecified, Modifier.padding(1.dp, 1.dp, 0.dp, 0.dp))
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
fun formattedDateTime(timestamp: Long): String {
|
||||||
return Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault())
|
return Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault())
|
||||||
.format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a"))
|
.format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a"))
|
||||||
|
|||||||
@@ -62,9 +62,9 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
|||||||
return TypedFilter(
|
return TypedFilter(
|
||||||
types = COMMON_FEED_TYPES,
|
types = COMMON_FEED_TYPES,
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(BookmarkListEvent.kind),
|
kinds = listOf(BookmarkListEvent.kind, PeopleListEvent.kind),
|
||||||
authors = listOf(account.userProfile().pubkeyHex),
|
authors = listOf(account.userProfile().pubkeyHex),
|
||||||
limit = 1
|
limit = 100
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ abstract class NostrDataSource(val debugName: String) {
|
|||||||
is LongTextNoteEvent -> LocalCache.consume(event, relay)
|
is LongTextNoteEvent -> LocalCache.consume(event, relay)
|
||||||
is MetadataEvent -> LocalCache.consume(event)
|
is MetadataEvent -> LocalCache.consume(event)
|
||||||
is PrivateDmEvent -> LocalCache.consume(event, relay)
|
is PrivateDmEvent -> LocalCache.consume(event, relay)
|
||||||
|
is PeopleListEvent -> LocalCache.consume(event)
|
||||||
is ReactionEvent -> LocalCache.consume(event)
|
is ReactionEvent -> LocalCache.consume(event)
|
||||||
is RecommendRelayEvent -> LocalCache.consume(event)
|
is RecommendRelayEvent -> LocalCache.consume(event)
|
||||||
is ReportEvent -> LocalCache.consume(event, relay)
|
is ReportEvent -> LocalCache.consume(event, relay)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createFollowAccountsFilter(): TypedFilter {
|
fun createFollowAccountsFilter(): TypedFilter {
|
||||||
val follows = account.followingKeySet()
|
val follows = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||||
|
|
||||||
val followKeys = follows.map {
|
val followKeys = follows.map {
|
||||||
it.substring(0, 6)
|
it.substring(0, 6)
|
||||||
@@ -65,7 +65,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createFollowTagsFilter(): TypedFilter? {
|
fun createFollowTagsFilter(): TypedFilter? {
|
||||||
val hashToLoad = account.followingTagSet()
|
val hashToLoad = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||||
|
|
||||||
if (hashToLoad.isEmpty()) return null
|
if (hashToLoad.isEmpty()) return null
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
|
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
|
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
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
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
|
|
||||||
object NostrVideoDataSource : NostrDataSource("VideoFeed") {
|
object NostrVideoDataSource : NostrDataSource("VideoFeed") {
|
||||||
fun createGlobalFilter() = TypedFilter(
|
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),
|
types = setOf(FeedType.GLOBAL),
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
|
authors = followKeys,
|
||||||
kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind),
|
kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind),
|
||||||
limit = 200
|
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()
|
val videoFeedChannel = requestNewChannel()
|
||||||
|
|
||||||
override fun updateChannelFilters() {
|
override fun updateChannelFilters() {
|
||||||
videoFeedChannel.typedFilters = listOf(createGlobalFilter()).ifEmpty { null }
|
videoFeedChannel.typedFilters = listOfNotNull(createContextualFilter(), createFollowTagsFilter()).ifEmpty { null }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
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.HexKey
|
||||||
import com.vitorpamplona.amethyst.model.toByteArray
|
|
||||||
import com.vitorpamplona.amethyst.model.toHexKey
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -15,50 +12,7 @@ class BookmarkListEvent(
|
|||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : GeneralListEvent(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<List<String>>? = null
|
|
||||||
|
|
||||||
fun privateTags(privKey: ByteArray): List<List<String>>? {
|
|
||||||
if (privateTagsCache != null) {
|
|
||||||
return privateTagsCache
|
|
||||||
}
|
|
||||||
|
|
||||||
privateTagsCache = try {
|
|
||||||
gson.fromJson(plainContent(privKey), object : TypeToken<List<List<String>>>() {}.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
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 30001
|
const val kind = 30001
|
||||||
|
|
||||||
@@ -77,24 +31,7 @@ class BookmarkListEvent(
|
|||||||
createdAt: Long = Date().time / 1000
|
createdAt: Long = Date().time / 1000
|
||||||
): BookmarkListEvent {
|
): BookmarkListEvent {
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
val pubKey = Utils.pubkeyCreate(privateKey)
|
||||||
|
val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey)
|
||||||
val privTags = mutableListOf<List<String>>()
|
|
||||||
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 tags = mutableListOf<List<String>>()
|
val tags = mutableListOf<List<String>>()
|
||||||
tags.add(listOf("d", name))
|
tags.add(listOf("d", name))
|
||||||
|
|||||||
@@ -238,6 +238,7 @@ open class Event(
|
|||||||
LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
MetadataEvent.kind -> MetadataEvent(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)
|
PollNoteEvent.kind -> PollNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig)
|
PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig)
|
ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
|||||||
@@ -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<List<String>>,
|
||||||
|
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<List<String>>? = null
|
||||||
|
|
||||||
|
fun privateTags(privKey: ByteArray): List<List<String>>? {
|
||||||
|
if (privateTagsCache != null) {
|
||||||
|
return privateTagsCache
|
||||||
|
}
|
||||||
|
|
||||||
|
privateTagsCache = try {
|
||||||
|
gson.fromJson(plainContent(privKey), object : TypeToken<List<List<String>>>() {}.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<String>? = null,
|
||||||
|
privUsers: List<String>? = null,
|
||||||
|
privAddresses: List<ATag>? = null,
|
||||||
|
|
||||||
|
privateKey: ByteArray,
|
||||||
|
pubKey: ByteArray
|
||||||
|
): String {
|
||||||
|
val privTags = mutableListOf<List<String>>()
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
companion object {
|
||||||
|
const val kind = 30000
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
name: String = "",
|
||||||
|
|
||||||
|
events: List<String>? = null,
|
||||||
|
users: List<String>? = null,
|
||||||
|
addresses: List<ATag>? = null,
|
||||||
|
|
||||||
|
privEvents: List<String>? = null,
|
||||||
|
privUsers: List<String>? = null,
|
||||||
|
privAddresses: List<ATag>? = 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<List<String>>()
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,20 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.actions
|
package com.vitorpamplona.amethyst.ui.actions
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import android.util.Log
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.vitorpamplona.amethyst.BuildConfig
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
import okio.BufferedSink
|
import okio.BufferedSink
|
||||||
import okio.source
|
import okio.source
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@@ -21,46 +27,68 @@ object ImageUploader {
|
|||||||
fun uploadImage(
|
fun uploadImage(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
server: ServersAvailable,
|
server: ServersAvailable,
|
||||||
|
context: Context,
|
||||||
contentResolver: ContentResolver,
|
contentResolver: ContentResolver,
|
||||||
onSuccess: (String, String?) -> Unit,
|
onSuccess: (String, String?) -> Unit,
|
||||||
onError: (Throwable) -> Unit
|
onError: (Throwable) -> Unit
|
||||||
) {
|
) {
|
||||||
val contentType = contentResolver.getType(uri)
|
val contentType = contentResolver.getType(uri)
|
||||||
val imageInputStream = contentResolver.openInputStream(uri)
|
val imageInputStream = contentResolver.openInputStream(uri)
|
||||||
|
|
||||||
checkNotNull(imageInputStream) {
|
checkNotNull(imageInputStream) {
|
||||||
"Can't open the image input stream"
|
"Can't open the image input stream"
|
||||||
}
|
}
|
||||||
|
|
||||||
val myServer = if (server == ServersAvailable.IMGUR) {
|
val myServer = if (server == ServersAvailable.IMGUR) {
|
||||||
ImgurServer()
|
ImgurServer()
|
||||||
} else if (server == ServersAvailable.NOSTRIMG) {
|
} else if (server == ServersAvailable.NOSTRIMG) {
|
||||||
NostrImgServer()
|
NostrImgServer()
|
||||||
|
} else if (server == ServersAvailable.NOSTR_BUILD) {
|
||||||
|
NostrBuildServer()
|
||||||
} else {
|
} else {
|
||||||
ImgurServer()
|
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(
|
fun uploadImage(
|
||||||
|
file: File,
|
||||||
inputStream: InputStream,
|
inputStream: InputStream,
|
||||||
contentType: String?,
|
contentType: String?,
|
||||||
server: FileServer,
|
server: FileServer,
|
||||||
|
serverType: ServersAvailable,
|
||||||
onSuccess: (String, String?) -> Unit,
|
onSuccess: (String, String?) -> Unit,
|
||||||
onError: (Throwable) -> Unit
|
onError: (Throwable) -> Unit
|
||||||
) {
|
) {
|
||||||
val category = contentType?.toMediaType()?.toString()?.split("/")?.get(0) ?: "image"
|
val category = contentType?.toMediaType()?.toString()?.split("/")?.get(0) ?: "image"
|
||||||
|
|
||||||
val fileName = randomChars()
|
val fileName = randomChars()
|
||||||
val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
||||||
|
|
||||||
val client = OkHttpClient.Builder().build()
|
val client = OkHttpClient.Builder().build()
|
||||||
|
val requestBody: RequestBody
|
||||||
|
val requestBuilder = Request.Builder()
|
||||||
|
|
||||||
val requestBody: RequestBody = MultipartBody.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)
|
.setType(MultipartBody.FORM)
|
||||||
.addFormDataPart(
|
.addFormDataPart(
|
||||||
category,
|
category,
|
||||||
"$fileName.$extension",
|
"$fileName.$extension",
|
||||||
|
|
||||||
object : RequestBody() {
|
object : RequestBody() {
|
||||||
override fun contentType(): MediaType? =
|
override fun contentType(): MediaType? =
|
||||||
contentType?.toMediaType()
|
contentType?.toMediaType()
|
||||||
@@ -71,18 +99,16 @@ object ImageUploader {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
}
|
||||||
val requestBuilder = Request.Builder()
|
|
||||||
|
|
||||||
server.clientID()?.let {
|
server.clientID()?.let {
|
||||||
requestBuilder.header("Authorization", it)
|
requestBuilder.addHeader("Authorization", it)
|
||||||
}
|
}
|
||||||
|
|
||||||
requestBuilder
|
requestBuilder
|
||||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
.addHeader("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||||
.url(server.postUrl(contentType))
|
.url(server.postUrl(contentType))
|
||||||
.post(requestBody)
|
.post(requestBody)
|
||||||
|
|
||||||
val request = requestBuilder.build()
|
val request = requestBuilder.build()
|
||||||
|
|
||||||
client.newCall(request).enqueue(object : Callback {
|
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 class FileServer {
|
||||||
abstract fun postUrl(contentType: String?): String
|
abstract fun postUrl(contentType: String?): String
|
||||||
abstract fun parseUrlFromSucess(body: String): String?
|
abstract fun parseUrlFromSucess(body: String): String?
|
||||||
@@ -146,12 +204,10 @@ class ImgurServer : FileServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NostrBuildServer : 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? {
|
override fun parseUrlFromSucess(body: String): String? {
|
||||||
val tree = jacksonObjectMapper().readTree(body)
|
val url = jacksonObjectMapper().readTree(body) // return url.toString()
|
||||||
val url = tree?.get("data")?.get("link")?.asText()
|
return url.toString().replace("\"", "")
|
||||||
return url
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clientID() = null
|
override fun clientID() = null
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ open class NewMediaModel : ViewModel() {
|
|||||||
selectedServer = ServersAvailable.IMGUR_NIP_94
|
selectedServer = ServersAvailable.IMGUR_NIP_94
|
||||||
} else if (selectedServer == ServersAvailable.NOSTRIMG) {
|
} else if (selectedServer == ServersAvailable.NOSTRIMG) {
|
||||||
selectedServer = ServersAvailable.NOSTRIMG_NIP_94
|
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(
|
ImageUploader.uploadImage(
|
||||||
uri = uri,
|
uri = uri,
|
||||||
server = serverToUse,
|
server = serverToUse,
|
||||||
|
context = context,
|
||||||
contentResolver = contentResolver,
|
contentResolver = contentResolver,
|
||||||
onSuccess = { imageUrl, mimeType ->
|
onSuccess = { imageUrl, mimeType ->
|
||||||
createNIP94Record(imageUrl, mimeType, description)
|
createNIP94Record(imageUrl, mimeType, description)
|
||||||
@@ -135,7 +138,7 @@ open class NewMediaModel : ViewModel() {
|
|||||||
onReady = {
|
onReady = {
|
||||||
uploadingPercentage.value = 0.90f
|
uploadingPercentage.value = 0.90f
|
||||||
uploadingDescription.value = "Sending"
|
uploadingDescription.value = "Sending"
|
||||||
val note = account?.sendHeader(it)
|
account?.sendHeader(it)
|
||||||
uploadingPercentage.value = 1.00f
|
uploadingPercentage.value = 1.00f
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
onceUploaded()
|
onceUploaded()
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, acc: Account) {
|
|||||||
val fileServers = listOf(
|
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.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.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))
|
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 ||
|
if (postViewModel.selectedServer == ServersAvailable.NOSTRIMG_NIP_94 ||
|
||||||
postViewModel.selectedServer == ServersAvailable.IMGUR_NIP_94 ||
|
postViewModel.selectedServer == ServersAvailable.IMGUR_NIP_94 ||
|
||||||
|
postViewModel.selectedServer == ServersAvailable.NOSTR_BUILD_NIP_94 ||
|
||||||
postViewModel.selectedServer == ServersAvailable.NIP95
|
postViewModel.selectedServer == ServersAvailable.NIP95
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -590,6 +590,7 @@ enum class ServersAvailable {
|
|||||||
NOSTRIMG,
|
NOSTRIMG,
|
||||||
IMGUR_NIP_94,
|
IMGUR_NIP_94,
|
||||||
NOSTRIMG_NIP_94,
|
NOSTRIMG_NIP_94,
|
||||||
|
NOSTR_BUILD_NIP_94,
|
||||||
NIP95
|
NIP95
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,8 +611,10 @@ fun ImageVideoDescription(
|
|||||||
val fileServers = listOf(
|
val fileServers = listOf(
|
||||||
Triple(ServersAvailable.IMGUR, stringResource(id = R.string.upload_server_imgur), stringResource(id = R.string.upload_server_imgur_explainer)),
|
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.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.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.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))
|
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 ||
|
if (selectedServer == ServersAvailable.NOSTRIMG_NIP_94 ||
|
||||||
selectedServer == ServersAvailable.IMGUR_NIP_94 ||
|
selectedServer == ServersAvailable.IMGUR_NIP_94 ||
|
||||||
|
selectedServer == ServersAvailable.NOSTR_BUILD_NIP_94 ||
|
||||||
selectedServer == ServersAvailable.NIP95
|
selectedServer == ServersAvailable.NIP95
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -145,9 +145,10 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
ImageUploader.uploadImage(
|
ImageUploader.uploadImage(
|
||||||
uri = it,
|
uri = it,
|
||||||
server = server,
|
server = server,
|
||||||
|
context = context,
|
||||||
contentResolver = contentResolver,
|
contentResolver = contentResolver,
|
||||||
onSuccess = { imageUrl, mimeType ->
|
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)
|
createNIP94Record(imageUrl, mimeType, description)
|
||||||
} else {
|
} else {
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ class NewUserMetadataViewModel : ViewModel() {
|
|||||||
ImageUploader.uploadImage(
|
ImageUploader.uploadImage(
|
||||||
uri = it,
|
uri = it,
|
||||||
server = account.defaultFileServer,
|
server = account.defaultFileServer,
|
||||||
|
context = context,
|
||||||
contentResolver = context.contentResolver,
|
contentResolver = context.contentResolver,
|
||||||
onSuccess = { imageUrl, mimeType ->
|
onSuccess = { imageUrl, mimeType ->
|
||||||
onUploading(false)
|
onUploading(false)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ data class ResizeImage(val url: String?, val size: Dp) {
|
|||||||
val imgPx = 200 // with(LocalDensity.current) { model.size.toPx().toInt() }
|
val imgPx = 200 // with(LocalDensity.current) { model.size.toPx().toInt() }
|
||||||
val base64 = Base64.getUrlEncoder().encodeToString(url.toByteArray())
|
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,10 +29,18 @@ import androidx.compose.ui.focus.FocusRequester
|
|||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TextSpinner(label: String, placeholder: String, options: List<String>, explainers: List<String>? = null, onSelect: (Int) -> Unit, modifier: Modifier = Modifier) {
|
fun TextSpinner(
|
||||||
|
label: String,
|
||||||
|
placeholder: String,
|
||||||
|
options: List<String>,
|
||||||
|
explainers: List<String>? = null,
|
||||||
|
onSelect: (Int) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
var optionsShowing by remember { mutableStateOf(false) }
|
var optionsShowing by remember { mutableStateOf(false) }
|
||||||
@@ -106,7 +114,7 @@ fun SpinnerSelectionDialog(options: List<String>, explainers: List<String>?, onD
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text(text = it, color = Color.Gray)
|
Text(text = it, color = Color.Gray, fontSize = 14.sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ package com.vitorpamplona.amethyst.ui.components
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
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.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
@@ -13,6 +10,7 @@ import androidx.compose.foundation.clickable
|
|||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
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.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.isFinite
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
@@ -173,11 +172,6 @@ fun ZoomableContentView(content: ZoomableContent, images: List<ZoomableContent>
|
|||||||
RoundedCornerShape(15.dp)
|
RoundedCornerShape(15.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
val ratio = aspectRatio(content.dim)
|
|
||||||
if (ratio != null) {
|
|
||||||
mainImageModifier = mainImageModifier.aspectRatio(ratio)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content is ZoomableUrlContent) {
|
if (content is ZoomableUrlContent) {
|
||||||
mainImageModifier = mainImageModifier.combinedClickable(
|
mainImageModifier = mainImageModifier.combinedClickable(
|
||||||
onClick = { dialogOpen = true },
|
onClick = { dialogOpen = true },
|
||||||
@@ -216,13 +210,22 @@ private fun LocalImageView(
|
|||||||
mutableStateOf<AsyncImagePainter.State?>(null)
|
mutableStateOf<AsyncImagePainter.State?>(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()) {
|
if (content.localFile.exists()) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = content.localFile,
|
model = content.localFile,
|
||||||
contentDescription = content.description,
|
contentDescription = content.description,
|
||||||
contentScale = ContentScale.FillWidth,
|
contentScale = contentScale,
|
||||||
modifier = mainImageModifier,
|
modifier = myModifier,
|
||||||
onLoading = {
|
onLoading = {
|
||||||
imageState = it
|
imageState = it
|
||||||
},
|
},
|
||||||
@@ -234,14 +237,9 @@ private fun LocalImageView(
|
|||||||
|
|
||||||
if (imageState is AsyncImagePainter.State.Success) {
|
if (imageState is AsyncImagePainter.State.Success) {
|
||||||
HashVerificationSymbol(content.isVerified, Modifier.align(Alignment.TopEnd))
|
HashVerificationSymbol(content.isVerified, Modifier.align(Alignment.TopEnd))
|
||||||
}
|
} else {
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = imageState !is AsyncImagePainter.State.Success,
|
|
||||||
exit = fadeOut(animationSpec = tween(200))
|
|
||||||
) {
|
|
||||||
if (content.blurhash != null) {
|
if (content.blurhash != null) {
|
||||||
DisplayBlurHash(content.blurhash, content.description, mainImageModifier)
|
DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier)
|
||||||
} else {
|
} else {
|
||||||
FlowRow() {
|
FlowRow() {
|
||||||
DisplayUrlWithLoadingSymbol(content)
|
DisplayUrlWithLoadingSymbol(content)
|
||||||
@@ -272,6 +270,8 @@ private fun UrlImageView(
|
|||||||
mutableStateOf<Boolean?>(null)
|
mutableStateOf<Boolean?>(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val ratio = aspectRatio(content.dim)
|
||||||
|
|
||||||
LaunchedEffect(key1 = content.url, key2 = imageState) {
|
LaunchedEffect(key1 = content.url, key2 = imageState) {
|
||||||
if (imageState is AsyncImagePainter.State.Success) {
|
if (imageState is AsyncImagePainter.State.Success) {
|
||||||
scope.launch(Dispatchers.IO) {
|
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(
|
AsyncImage(
|
||||||
model = content.url,
|
model = content.url,
|
||||||
contentDescription = content.description,
|
contentDescription = content.description,
|
||||||
contentScale = ContentScale.FillWidth,
|
contentScale = contentScale,
|
||||||
modifier = mainImageModifier,
|
modifier = myModifier,
|
||||||
onLoading = {
|
onLoading = {
|
||||||
imageState = it
|
imageState = it
|
||||||
},
|
},
|
||||||
@@ -296,14 +303,9 @@ private fun UrlImageView(
|
|||||||
|
|
||||||
if (imageState is AsyncImagePainter.State.Success) {
|
if (imageState is AsyncImagePainter.State.Success) {
|
||||||
HashVerificationSymbol(verifiedHash, Modifier.align(Alignment.TopEnd))
|
HashVerificationSymbol(verifiedHash, Modifier.align(Alignment.TopEnd))
|
||||||
}
|
} else {
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = imageState !is AsyncImagePainter.State.Success,
|
|
||||||
exit = fadeOut(animationSpec = tween(200))
|
|
||||||
) {
|
|
||||||
if (content.blurhash != null) {
|
if (content.blurhash != null) {
|
||||||
DisplayBlurHash(content.blurhash, content.description, mainImageModifier)
|
DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier)
|
||||||
} else {
|
} else {
|
||||||
FlowRow() {
|
FlowRow() {
|
||||||
DisplayUrlWithLoadingSymbol(content)
|
DisplayUrlWithLoadingSymbol(content)
|
||||||
@@ -389,6 +391,7 @@ private fun DisplayUrlWithLoadingSymbolWait(content: ZoomableContent) {
|
|||||||
private fun DisplayBlurHash(
|
private fun DisplayBlurHash(
|
||||||
blurhash: String?,
|
blurhash: String?,
|
||||||
description: String?,
|
description: String?,
|
||||||
|
contentScale: ContentScale,
|
||||||
modifier: Modifier
|
modifier: Modifier
|
||||||
) {
|
) {
|
||||||
if (blurhash == null) return
|
if (blurhash == null) return
|
||||||
@@ -400,7 +403,7 @@ private fun DisplayBlurHash(
|
|||||||
blurhash
|
blurhash
|
||||||
),
|
),
|
||||||
contentDescription = description,
|
contentDescription = description,
|
||||||
contentScale = ContentScale.FillWidth,
|
contentScale = contentScale,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,8 @@ object HomeConversationsFeedFilter : AdditiveFeedFilter<Note>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
val user = account.userProfile()
|
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||||
val followingKeySet = user.cachedFollowingKeySet()
|
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||||
val followingTagSet = user.cachedFollowingTagSet()
|
|
||||||
|
|
||||||
return collection
|
return collection
|
||||||
.asSequence()
|
.asSequence()
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ object HomeNewThreadFeedFilter : AdditiveFeedFilter<Note>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
val user = account.userProfile()
|
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||||
val followingKeySet = user.cachedFollowingKeySet()
|
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||||
val followingTagSet = user.cachedFollowingTagSet()
|
|
||||||
|
|
||||||
return collection
|
return collection
|
||||||
.asSequence()
|
.asSequence()
|
||||||
|
|||||||
@@ -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<Note>() {
|
||||||
|
lateinit var account: Account
|
||||||
|
|
||||||
|
override fun feed(): List<Note> {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.dal
|
package com.vitorpamplona.amethyst.ui.dal
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.model.*
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
@@ -20,17 +21,17 @@ object VideoFeedFilter : AdditiveFeedFilter<Note>() {
|
|||||||
|
|
||||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
val now = System.currentTimeMillis() / 1000
|
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
|
return collection
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.filter {
|
.filter { it.event is FileHeaderEvent || it.event is FileStorageHeaderEvent }
|
||||||
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 { account.isAcceptable(it) }
|
||||||
.filter {
|
.filter { it.createdAt()!! <= now }
|
||||||
// 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
|
|
||||||
}
|
|
||||||
.toSet()
|
.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.navigation
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -41,6 +44,9 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
import com.vitorpamplona.amethyst.R
|
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.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
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.NostrSingleUserDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
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.Client
|
||||||
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
|
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.components.RobohashAsyncImageProxy
|
||||||
import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel
|
import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppTopBar(navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun AppTopBar(navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
when (currentRoute(navController)) {
|
when (currentRoute(navController)?.substringBefore("?")) {
|
||||||
// Route.Profile.route -> TopBarWithBackButton(navController)
|
// Route.Profile.route -> TopBarWithBackButton(navController)
|
||||||
|
Route.Home.base -> HomeTopBar(scaffoldState, accountViewModel)
|
||||||
|
Route.Video.base -> StoriesTopBar(scaffoldState, accountViewModel)
|
||||||
else -> MainTopBar(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)
|
@OptIn(coil.annotation.ExperimentalCoilApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, content: @Composable (Account) -> Unit) {
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
val account = accountState?.account ?: return
|
val account = accountState?.account ?: return
|
||||||
|
|
||||||
val accountUserState by account.userProfile().live().metadata.observeAsState()
|
|
||||||
val accountUser = accountUserState?.user ?: return
|
|
||||||
|
|
||||||
val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() }
|
val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() }
|
||||||
val connectedRelaysLiveData by relayViewModel.connectedRelaysLiveData.observeAsState()
|
val connectedRelaysLiveData by relayViewModel.connectedRelaysLiveData.observeAsState()
|
||||||
val availableRelaysLiveData by relayViewModel.availableRelaysLiveData.observeAsState()
|
val availableRelaysLiveData by relayViewModel.availableRelaysLiveData.observeAsState()
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var wantsToEditRelays by remember {
|
var wantsToEditRelays by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
@@ -115,6 +147,193 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
|
content(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(),
|
||||||
|
horizontalAlignment = Alignment.End
|
||||||
|
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxHeight(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"${connectedRelaysLiveData ?: "--"}/${availableRelaysLiveData ?: "--"}",
|
||||||
|
color = if (connectedRelaysLiveData == 0) Color.Red else MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
|
style = MaterialTheme.typography.subtitle1,
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
onClick = {
|
||||||
|
wantsToEditRelays = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
LoggedInUserPictureDrawer(account) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
scaffoldState.drawerState.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(
|
||||||
|
onClick = { wantsToEditRelays = true },
|
||||||
|
modifier = Modifier
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.relays),
|
||||||
|
null,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = Color.Unspecified
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Divider(thickness = 0.25.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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<String>,
|
||||||
|
explainers: List<String>? = 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() {
|
||||||
|
TopAppBar(
|
||||||
|
elevation = 0.dp,
|
||||||
|
backgroundColor = Color(0xFFFFFF),
|
||||||
|
title = {},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.ArrowBack,
|
||||||
|
null,
|
||||||
|
modifier = Modifier.size(28.dp),
|
||||||
|
tint = MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {}
|
||||||
|
)
|
||||||
|
Divider(thickness = 0.25.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AmethystIcon() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
Client.allSubscriptions().map {
|
Client.allSubscriptions().map {
|
||||||
@@ -157,96 +376,4 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
|
|||||||
tint = Color.Unspecified
|
tint = Color.Unspecified
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.fillMaxHeight(),
|
|
||||||
horizontalAlignment = Alignment.End
|
|
||||||
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxHeight(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"${connectedRelaysLiveData ?: "--"}/${availableRelaysLiveData ?: "--"}",
|
|
||||||
color = if (connectedRelaysLiveData == 0) Color.Red else MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
|
||||||
style = MaterialTheme.typography.subtitle1,
|
|
||||||
modifier = Modifier.clickable(
|
|
||||||
onClick = {
|
|
||||||
wantsToEditRelays = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
IconButton(
|
|
||||||
onClick = { wantsToEditRelays = true },
|
|
||||||
modifier = Modifier
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.relays),
|
|
||||||
null,
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
tint = Color.Unspecified
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Divider(thickness = 0.25.dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TopBarWithBackButton(navController: NavHostController) {
|
|
||||||
Column() {
|
|
||||||
TopAppBar(
|
|
||||||
elevation = 0.dp,
|
|
||||||
backgroundColor = Color(0xFFFFFF),
|
|
||||||
title = {},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
navController.popBackStack()
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.ArrowBack,
|
|
||||||
null,
|
|
||||||
modifier = Modifier.size(28.dp),
|
|
||||||
tint = MaterialTheme.colors.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {}
|
|
||||||
)
|
|
||||||
Divider(thickness = 0.25.dp)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import com.google.accompanist.flowlayout.FlowRow
|
|||||||
import com.vitorpamplona.amethyst.NotificationCache
|
import com.vitorpamplona.amethyst.NotificationCache
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.Channel
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
@@ -202,8 +203,7 @@ fun NoteComposeInner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier.combinedClickable(
|
||||||
.combinedClickable(
|
|
||||||
onClick = {
|
onClick = {
|
||||||
if (noteEvent is ChannelMessageEvent) {
|
if (noteEvent is ChannelMessageEvent) {
|
||||||
baseChannel?.let {
|
baseChannel?.let {
|
||||||
@@ -229,86 +229,18 @@ fun NoteComposeInner(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClick = { popupExpanded = true }
|
onLongClick = { popupExpanded = true }
|
||||||
)
|
).background(backgroundColor)
|
||||||
.background(backgroundColor)
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(
|
.padding(
|
||||||
start = if (!isBoostedNote) 12.dp else 0.dp,
|
start = if (!isBoostedNote) 12.dp else 0.dp,
|
||||||
end = 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) {
|
if (!isBoostedNote && !isQuotedNote) {
|
||||||
Column(Modifier.width(55.dp)) {
|
DrawAuthorImages(baseNote, loggedIn, navController)
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
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) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
ObserveDisplayNip05Status(note.author!!, Modifier.weight(1f))
|
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())) {
|
if (!makeItShort && noteEvent is TextNoteEvent && (note.replyTo != null || noteEvent.mentions().isNotEmpty())) {
|
||||||
val replyingDirectlyTo = note.replyTo?.lastOrNull()
|
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
|
@Composable
|
||||||
fun DisplayHighlight(
|
fun DisplayHighlight(
|
||||||
highlight: String,
|
highlight: String,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@@ -50,9 +51,11 @@ fun HomeScreen(
|
|||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val account = accountViewModel.accountLiveData.value?.account ?: return
|
val account = accountViewModel.accountLiveData.value?.account ?: return
|
||||||
var wantsToAddNip47 by remember { mutableStateOf<String?>(nip47) }
|
var wantsToAddNip47 by remember { mutableStateOf(nip47) }
|
||||||
|
|
||||||
LaunchedEffect(accountViewModel) {
|
val accountState = account.live.observeAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(accountViewModel, accountState.value?.account?.defaultHomeFollowList) {
|
||||||
HomeNewThreadFeedFilter.account = account
|
HomeNewThreadFeedFilter.account = account
|
||||||
HomeConversationsFeedFilter.account = account
|
HomeConversationsFeedFilter.account = account
|
||||||
NostrHomeDataSource.resetFilters()
|
NostrHomeDataSource.resetFilters()
|
||||||
|
|||||||
@@ -105,10 +105,14 @@ fun VideoScreen(
|
|||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
val account = accountViewModel.accountLiveData.value?.account ?: return
|
val account = accountViewModel.accountLiveData.value?.account ?: return
|
||||||
|
|
||||||
|
val accountState = account.live.observeAsState()
|
||||||
|
|
||||||
|
NostrVideoDataSource.account = account
|
||||||
VideoFeedFilter.account = account
|
VideoFeedFilter.account = account
|
||||||
|
|
||||||
LaunchedEffect(accountViewModel) {
|
LaunchedEffect(accountViewModel, accountState.value?.account?.defaultStoriesFollowList) {
|
||||||
VideoFeedFilter.account = account
|
VideoFeedFilter.account = account
|
||||||
|
NostrVideoDataSource.account = account
|
||||||
NostrVideoDataSource.resetFilters()
|
NostrVideoDataSource.resetFilters()
|
||||||
videoFeedView.invalidateData()
|
videoFeedView.invalidateData()
|
||||||
}
|
}
|
||||||
@@ -118,6 +122,7 @@ fun VideoScreen(
|
|||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
println("Video Start")
|
println("Video Start")
|
||||||
VideoFeedFilter.account = account
|
VideoFeedFilter.account = account
|
||||||
|
NostrVideoDataSource.account = account
|
||||||
NostrVideoDataSource.start()
|
NostrVideoDataSource.start()
|
||||||
videoFeedView.invalidateData()
|
videoFeedView.invalidateData()
|
||||||
}
|
}
|
||||||
@@ -237,7 +242,7 @@ private fun RenderVideoOrPictureNote(
|
|||||||
var moreActionsExpanded by remember { mutableStateOf(false) }
|
var moreActionsExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Column(Modifier.fillMaxSize(1f)) {
|
Column(Modifier.fillMaxSize(1f)) {
|
||||||
Row(Modifier.weight(1f, true), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {
|
||||||
if (noteEvent is FileHeaderEvent) {
|
if (noteEvent is FileHeaderEvent) {
|
||||||
FileHeaderDisplay(note)
|
FileHeaderDisplay(note)
|
||||||
} else if (noteEvent is FileStorageHeaderEvent) {
|
} else if (noteEvent is FileStorageHeaderEvent) {
|
||||||
|
|||||||
@@ -334,4 +334,10 @@
|
|||||||
<string name="upload_server_relays_nip95">Saját csomópontjaid (NIP-95)</string>
|
<string name="upload_server_relays_nip95">Saját csomópontjaid (NIP-95)</string>
|
||||||
<string name="upload_server_relays_nip95_explainer">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</string>
|
<string name="upload_server_relays_nip95_explainer">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</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<string name="follow_list_selection">Követek Lista</string>
|
||||||
|
<string name="follow_list_kind3follows">Mindenki akit követek</string>
|
||||||
|
<string name="follow_list_global">Globális</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -280,9 +280,56 @@
|
|||||||
<string name="looking_for_event">"Zoeken naar event %1$s"</string>
|
<string name="looking_for_event">"Zoeken naar event %1$s"</string>
|
||||||
|
|
||||||
<string name="custom_zaps_add_a_message">Een openbaar bericht toevoegen</string>
|
<string name="custom_zaps_add_a_message">Een openbaar bericht toevoegen</string>
|
||||||
|
<string name="custom_zaps_add_a_message_private">Een privébericht toevoegen</string>
|
||||||
|
<string name="custom_zaps_add_a_message_nonzap">Een invoice bericht toevoegen</string>
|
||||||
<string name="custom_zaps_add_a_message_example">Bedankt voor al jullie werk!</string>
|
<string name="custom_zaps_add_a_message_example">Bedankt voor al jullie werk!</string>
|
||||||
|
|
||||||
<string name="lightning_create_and_add_invoice">Maken en toevoegen</string>
|
<string name="lightning_create_and_add_invoice">Maken en toevoegen</string>
|
||||||
<string name="poll_author_no_vote">Poll auteurs kunnen niet stemmen in hun eigen poll.</string>
|
<string name="poll_author_no_vote">Poll auteurs kunnen niet stemmen in hun eigen poll.</string>
|
||||||
<string name="poll_hashtag" translatable="false">#zappoll</string>
|
<string name="poll_hashtag" translatable="false">#zappoll</string>
|
||||||
|
|
||||||
|
<string name="hash_verification_passed">De afbeelding is onveranderd sinds de post</string>
|
||||||
|
<string name="hash_verification_failed">De afbeelding is veranderd. De auteur heeft de wijziging misschien niet gezien</string>
|
||||||
|
|
||||||
|
<string name="content_description_add_image">Afbeelding toevoegen</string>
|
||||||
|
<string name="content_description_add_video">Video toevoegen</string>
|
||||||
|
<string name="content_description_add_document">Document toevoegen</string>
|
||||||
|
|
||||||
|
<string name="add_content">Voeg toe aan bericht</string>
|
||||||
|
<string name="content_description">Beschrijving van de inhoud</string>
|
||||||
|
<string name="content_description_example">Een blauwe boot in een wit zandstrand bij zonsondergang</string>
|
||||||
|
|
||||||
|
<string name="zap_type">Zap type</string>
|
||||||
|
<string name="zap_type_explainer">Zap type voor alle opties</string>
|
||||||
|
|
||||||
|
<string name="zap_type_public">Publiek</string>
|
||||||
|
<string name="zap_type_public_explainer">Iedereen kan de transactie en het bericht zien</string>
|
||||||
|
|
||||||
|
<string name="zap_type_private">Privé</string>
|
||||||
|
<string name="zap_type_private_explainer">Verzender en ontvanger kunnen elkaar zien en het bericht lezen</string>
|
||||||
|
|
||||||
|
<string name="zap_type_anonymous">Anoniem</string>
|
||||||
|
<string name="zap_type_anonymous_explainer">Ontvanger en het publiek weten niet wie de betaling verzond</string>
|
||||||
|
|
||||||
|
<string name="zap_type_nonzap">Non-Zap</string>
|
||||||
|
<string name="zap_type_nonzap_explainer">Geen spoor binnen Nostr, alleen in Lightning</string>
|
||||||
|
|
||||||
|
|
||||||
|
<string name="file_server">File server</string>
|
||||||
|
<string name="zap_forward_lnAddress">LnAddress of @User</string>
|
||||||
|
|
||||||
|
<string name="upload_server_imgur">imgur.com - minder beveiligd</string>
|
||||||
|
<string name="upload_server_imgur_explainer">Upload naar Imgur. Imgur kan uw afbeelding/video op elk moment wijzigen</string>
|
||||||
|
|
||||||
|
<string name="upload_server_nostrimg">nostrimg.com - minder beveiligd</string>
|
||||||
|
<string name="upload_server_nostrimg_explainer">Upload naar NostrImg. NostrImg kan uw afbeelding/video op elk moment wijzigen</string>
|
||||||
|
|
||||||
|
<string name="upload_server_imgur_nip94">Verifieerbare Imgur (NIP-94)</string>
|
||||||
|
<string name="upload_server_imgur_nip94_explainer">Beschermt tegen het achteraf wijzigen van de inhoud door Imgur. Dit is een nieuwe NIP: andere clients zien de afbeelding misschien niet.</string>
|
||||||
|
|
||||||
|
<string name="upload_server_nostrimg_nip94">Verifieerbare NostrImg (NIP-94)</string>
|
||||||
|
<string name="upload_server_nostrimg_nip94_explainer">Beschermt tegen het achteraf wijzigen van de inhoud door NostrImg. Dit is een nieuwe NIP: andere clients zien de afbeelding misschien niet.</string>
|
||||||
|
|
||||||
|
<string name="upload_server_relays_nip95">Uw relays (NIP-95)</string>
|
||||||
|
<string name="upload_server_relays_nip95_explainer">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.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -327,19 +327,31 @@
|
|||||||
<string name="file_server">File Server</string>
|
<string name="file_server">File Server</string>
|
||||||
<string name="zap_forward_lnAddress">LnAddress or @User</string>
|
<string name="zap_forward_lnAddress">LnAddress or @User</string>
|
||||||
|
|
||||||
<string name="upload_server_imgur">imgur.com - less secure</string>
|
<string name="upload_server_imgur">imgur.com - trusted</string>
|
||||||
<string name="upload_server_imgur_explainer">Uploads to Imgur. Imgur can change your image/video at any time</string>
|
<string name="upload_server_imgur_explainer">Imgur can modify the file</string>
|
||||||
|
|
||||||
<string name="upload_server_nostrimg">nostrimg.com - less secure</string>
|
<string name="upload_server_nostrimg">nostrimg.com - trusted</string>
|
||||||
<string name="upload_server_nostrimg_explainer">Uploads to NostrImg. NostrImg can change your image at any time</string>
|
<string name="upload_server_nostrimg_explainer">NostrImg can modify the file</string>
|
||||||
|
|
||||||
|
<string name="upload_server_nostrbuild">nostr.build - trusted</string>
|
||||||
|
<string name="upload_server_nostrbuild_explainer">Nostr.build can modify the file</string>
|
||||||
|
|
||||||
<string name="upload_server_imgur_nip94">Verifiable Imgur (NIP-94)</string>
|
<string name="upload_server_imgur_nip94">Verifiable Imgur (NIP-94)</string>
|
||||||
<string name="upload_server_imgur_nip94_explainer">Protects from Imgur modifying the content afterwards. This is a new NIP: other clients might not see the image</string>
|
<string name="upload_server_imgur_nip94_explainer">Checks if Imgur modified the file. New NIP: other clients might not see it</string>
|
||||||
|
|
||||||
<string name="upload_server_nostrimg_nip94">Verifiable NostrImg (NIP-94)</string>
|
<string name="upload_server_nostrimg_nip94">Verifiable NostrImg (NIP-94)</string>
|
||||||
<string name="upload_server_nostrimg_nip94_explainer">Protects from NostrImg modifying the content afterwards. This is a new NIP: other clients might not see the image</string>
|
<string name="upload_server_nostrimg_nip94_explainer">Checks if NostrImg modified the file. New NIP: other clients might not see it</string>
|
||||||
|
|
||||||
|
<string name="upload_server_nostrbuild_nip94">Verifiable Nostr.build (NIP-94)</string>
|
||||||
|
<string name="upload_server_nostrbuild_nip94_explainer">Checks if Nostr.build modified the file. New NIP: other clients might not see it</string>
|
||||||
|
|
||||||
<string name="upload_server_relays_nip95">Your relays (NIP-95)</string>
|
<string name="upload_server_relays_nip95">Your relays (NIP-95)</string>
|
||||||
<string name="upload_server_relays_nip95_explainer">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</string>
|
<string name="upload_server_relays_nip95_explainer">Files are hosted by your relays. New NIP: check if they support</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<string name="follow_list_selection">Follow List</string>
|
||||||
|
<string name="follow_list_kind3follows">All Follows</string>
|
||||||
|
<string name="follow_list_global">Global</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user