mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-06 14:54:36 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fadab5bb9 | ||
|
|
ea768fb560 | ||
|
|
71cb18f360 | ||
|
|
3a1694ac9a | ||
|
|
91c47b6e87 | ||
|
|
fe4e03df39 | ||
|
|
d110b9fbbf | ||
|
|
dc8c177f20 | ||
|
|
1beacd240e | ||
|
|
083394b928 | ||
|
|
6de8d9d7e0 | ||
|
|
fb4457e767 | ||
|
|
d4c1907349 | ||
|
|
c92926f3ab |
+2
-2
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 233
|
||||
versionName "0.64.2"
|
||||
versionCode 236
|
||||
versionName "0.65.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@@ -55,6 +55,7 @@ private object PrefKeys {
|
||||
const val DEFAULT_HOME_FOLLOW_LIST = "defaultHomeFollowList"
|
||||
const val DEFAULT_STORIES_FOLLOW_LIST = "defaultStoriesFollowList"
|
||||
const val DEFAULT_NOTIFICATION_FOLLOW_LIST = "defaultNotificationFollowList"
|
||||
const val DEFAULT_DISCOVERY_FOLLOW_LIST = "defaultDiscoveryFollowList"
|
||||
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer"
|
||||
const val LATEST_CONTACT_LIST = "latestContactList"
|
||||
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
|
||||
@@ -216,6 +217,7 @@ object LocalPreferences {
|
||||
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, account.defaultHomeFollowList)
|
||||
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, account.defaultStoriesFollowList)
|
||||
putString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, account.defaultNotificationFollowList)
|
||||
putString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, account.defaultDiscoveryFollowList)
|
||||
putString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, gson.toJson(account.zapPaymentRequest))
|
||||
putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList))
|
||||
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
|
||||
@@ -256,6 +258,7 @@ object LocalPreferences {
|
||||
val defaultHomeFollowList = getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: KIND3_FOLLOWS
|
||||
val defaultStoriesFollowList = getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
||||
val defaultNotificationFollowList = getString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
||||
val defaultDiscoveryFollowList = getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
||||
|
||||
val zapAmountChoices = gson.fromJson(
|
||||
getString(PrefKeys.ZAP_AMOUNTS, "[]"),
|
||||
@@ -349,6 +352,7 @@ object LocalPreferences {
|
||||
defaultHomeFollowList = defaultHomeFollowList,
|
||||
defaultStoriesFollowList = defaultStoriesFollowList,
|
||||
defaultNotificationFollowList = defaultNotificationFollowList,
|
||||
defaultDiscoveryFollowList = defaultDiscoveryFollowList,
|
||||
zapPaymentRequest = zapPaymentRequestServer,
|
||||
hideDeleteRequestDialog = hideDeleteRequestDialog,
|
||||
hideBlockAlertDialog = hideBlockAlertDialog,
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrDiscoveryDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
||||
@@ -66,6 +67,7 @@ object ServiceManager {
|
||||
NostrHomeDataSource.account = myAccount
|
||||
NostrChatroomListDataSource.account = myAccount
|
||||
NostrVideoDataSource.account = myAccount
|
||||
NostrDiscoveryDataSource.account = myAccount
|
||||
ImageUploader.account = myAccount
|
||||
|
||||
// Notification Elements
|
||||
@@ -87,6 +89,7 @@ object ServiceManager {
|
||||
NostrHomeDataSource.stop()
|
||||
NostrChannelDataSource.stop()
|
||||
NostrChatroomListDataSource.stop()
|
||||
NostrDiscoveryDataSource.stop()
|
||||
|
||||
NostrSingleChannelDataSource.stop()
|
||||
NostrSingleEventDataSource.stop()
|
||||
|
||||
@@ -62,6 +62,7 @@ class Account(
|
||||
var defaultHomeFollowList: String = KIND3_FOLLOWS,
|
||||
var defaultStoriesFollowList: String = GLOBAL_FOLLOWS,
|
||||
var defaultNotificationFollowList: String = GLOBAL_FOLLOWS,
|
||||
var defaultDiscoveryFollowList: String = GLOBAL_FOLLOWS,
|
||||
var zapPaymentRequest: Nip47URI? = null,
|
||||
var hideDeleteRequestDialog: Boolean = false,
|
||||
var hideBlockAlertDialog: Boolean = false,
|
||||
@@ -899,6 +900,12 @@ class Account(
|
||||
saveable.invalidateData()
|
||||
}
|
||||
|
||||
fun changeDefaultDiscoveryFollowList(name: String) {
|
||||
defaultDiscoveryFollowList = name
|
||||
live.invalidateData()
|
||||
saveable.invalidateData()
|
||||
}
|
||||
|
||||
fun changeZapAmounts(newAmounts: List<Long>) {
|
||||
zapAmountChoices = newAmounts
|
||||
live.invalidateData()
|
||||
|
||||
@@ -7,18 +7,19 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
fun checkForHashtagWithIcon(tag: String, primary: Color): HashtagIcon? {
|
||||
return when (tag.lowercase()) {
|
||||
"bitcoin", "btc", "timechain" -> HashtagIcon(R.drawable.ht_btc, "Bitcoin", Color.Unspecified, Modifier.padding(2.dp, 2.dp, 0.dp, 0.dp))
|
||||
"bitcoin", "btc", "timechain", "bitcoiner", "bitcoiners" -> HashtagIcon(R.drawable.ht_btc, "Bitcoin", Color.Unspecified, Modifier.padding(2.dp, 2.dp, 0.dp, 0.dp))
|
||||
"nostr", "nostrich", "nostriches" -> HashtagIcon(R.drawable.ht_nostr, "Nostr", Color.Unspecified, Modifier.padding(1.dp, 2.dp, 0.dp, 0.dp))
|
||||
"lightning", "lightningnetwork" -> HashtagIcon(R.drawable.ht_lightning, "Lightning", Color.Unspecified, Modifier.padding(1.dp, 3.dp, 0.dp, 0.dp))
|
||||
"zap", "zaps", "zapper", "zappers", "zapping", "zapped", "zapathon", "zapraiser", "zaplife", "zapchain" -> HashtagIcon(R.drawable.zap, "Zap", Color.Unspecified, Modifier.padding(1.dp, 3.dp, 0.dp, 0.dp))
|
||||
"amethyst" -> HashtagIcon(R.drawable.amethyst, "Amethyst", Color.Unspecified, Modifier.padding(3.dp, 2.dp, 0.dp, 0.dp))
|
||||
"onyx" -> HashtagIcon(R.drawable.black_heart, "Onyx", Color.Unspecified, Modifier.padding(1.dp, 3.dp, 0.dp, 0.dp))
|
||||
"cashu", "ecash", "nut", "nuts", "deeznuts" -> HashtagIcon(R.drawable.cashu, "Cashu", Color.Unspecified, Modifier.padding(1.dp, 3.dp, 0.dp, 0.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))
|
||||
"skullofsatoshi" -> HashtagIcon(R.drawable.skull, "SkullofSatoshi", Color.Unspecified, Modifier.padding(2.dp, 1.dp, 0.dp, 0.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))
|
||||
"tunestr", "music", "nowplaying" -> HashtagIcon(R.drawable.tunestr, "Tunestr", primary, Modifier.padding(1.dp, 1.dp, 0.dp, 0.dp))
|
||||
"tunestr", "music", "nowplaying" -> HashtagIcon(R.drawable.tunestr, "Tunestr", primary, Modifier.padding(0.dp, 3.dp, 0.dp, 1.dp))
|
||||
"weed", "weedstr", "420", "cannabis", "marijuana" -> HashtagIcon(R.drawable.weed, "Weed", Color.Unspecified, Modifier.padding(0.dp, 0.dp, 0.dp, 0.dp))
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ object LocalCache {
|
||||
fun checkGetOrCreateUser(key: String): User? {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (isValidHexNpub(key)) {
|
||||
if (isValidHex(key)) {
|
||||
return getOrCreateUser(key)
|
||||
}
|
||||
return null
|
||||
@@ -69,7 +69,7 @@ object LocalCache {
|
||||
if (ATag.isATag(key)) {
|
||||
return checkGetOrCreateAddressableNote(key)
|
||||
}
|
||||
if (isValidHexNpub(key)) {
|
||||
if (isValidHex(key)) {
|
||||
val note = getOrCreateNote(key)
|
||||
val noteEvent = note.event
|
||||
if (noteEvent is AddressableEvent) {
|
||||
@@ -94,7 +94,7 @@ object LocalCache {
|
||||
fun checkGetOrCreateChannel(key: String): Channel? {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (isValidHexNpub(key)) {
|
||||
if (isValidHex(key)) {
|
||||
return getOrCreateChannel(key) {
|
||||
PublicChatChannel(key)
|
||||
}
|
||||
@@ -108,7 +108,7 @@ object LocalCache {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun isValidHexNpub(key: String): Boolean {
|
||||
private fun isValidHex(key: String): Boolean {
|
||||
if (key.isBlank()) return false
|
||||
if (key.contains(":")) return false
|
||||
|
||||
@@ -798,7 +798,7 @@ object LocalCache {
|
||||
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
||||
// Log.d("CM", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()} ${formattedDateTime(event.createdAt)}")
|
||||
Log.d("CM", "New Chat Note (${note.author?.toBestDisplayName()} ${note.event?.content()} ${formattedDateTime(event.createdAt)}")
|
||||
|
||||
// Counts the replies
|
||||
replyTo.forEach {
|
||||
|
||||
@@ -279,6 +279,20 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
}
|
||||
|
||||
fun publicZapAuthors(): Set<HexKey> {
|
||||
// Zaps who the requester was the user
|
||||
return zaps.mapNotNull {
|
||||
it.key.author?.pubkeyHex
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
fun reactionAuthors(): Set<HexKey> {
|
||||
// Zaps who the requester was the user
|
||||
return reactions.values.map {
|
||||
it.mapNotNull { it.author?.pubkeyHex }
|
||||
}.flatten().toSet()
|
||||
}
|
||||
|
||||
fun isReactedBy(user: User): String? {
|
||||
return reactions.filter {
|
||||
it.value.any { it.author?.pubkeyHex == user.pubkeyHex }
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
|
||||
object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
lateinit var account: Account
|
||||
|
||||
fun createContextualFilter(): TypedFilter {
|
||||
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
|
||||
|
||||
val followKeys = follows?.map {
|
||||
it.substring(0, 6)
|
||||
}
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter = JsonFilter(
|
||||
authors = followKeys,
|
||||
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind, LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind),
|
||||
limit = 1000
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createFollowTagsFilter(): TypedFilter? {
|
||||
val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList)
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind, LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind),
|
||||
tags = mapOf(
|
||||
"t" to hashToLoad.map {
|
||||
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
|
||||
}.flatten()
|
||||
),
|
||||
limit = 1000
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val discoveryFeedChannel = requestNewChannel()
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
discoveryFeedChannel.typedFilters = listOfNotNull(createContextualFilter(), createFollowTagsFilter()).ifEmpty { null }
|
||||
}
|
||||
}
|
||||
@@ -25,15 +25,27 @@ class LiveActivitiesEvent(
|
||||
fun streaming() = tags.firstOrNull { it.size > 1 && it[0] == "streaming" }?.get(1)
|
||||
fun starts() = tags.firstOrNull { it.size > 1 && it[0] == "starts" }?.get(1)
|
||||
fun ends() = tags.firstOrNull { it.size > 1 && it[0] == "ends" }?.get(1)
|
||||
fun status() = tags.firstOrNull { it.size > 1 && it[0] == "status" }?.get(1)
|
||||
fun status() = checkStatus(tags.firstOrNull { it.size > 1 && it[0] == "status" }?.get(1))
|
||||
fun currentParticipants() = tags.firstOrNull { it.size > 1 && it[0] == "current_participants" }?.get(1)
|
||||
fun totalParticipants() = tags.firstOrNull { it.size > 1 && it[0] == "total_participants" }?.get(1)
|
||||
|
||||
fun participants() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(2)) }
|
||||
|
||||
fun checkStatus(eventStatus: String?): String? {
|
||||
return if (eventStatus == STATUS_LIVE && createdAt < Date().time / 1000 - (60 * 60 * 8)) { // 2 hours {
|
||||
STATUS_ENDED
|
||||
} else {
|
||||
eventStatus
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val kind = 30311
|
||||
|
||||
const val STATUS_LIVE = "live"
|
||||
const val STATUS_PLANNED = "planned"
|
||||
const val STATUS_ENDED = "ended"
|
||||
|
||||
fun create(
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000
|
||||
|
||||
@@ -75,7 +75,6 @@ import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.ui.actions.SaveToGallery
|
||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font17SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.hashVerified
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -612,7 +611,7 @@ private fun HashVerificationSymbol(verifiedHash: Boolean, modifier: Modifier) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_verified),
|
||||
"Hash Verified",
|
||||
tint = MaterialTheme.colors.hashVerified,
|
||||
tint = Color.Unspecified,
|
||||
modifier = Modifier.size(30.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.model.*
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_ENDED
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_LIVE
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_PLANNED
|
||||
|
||||
class DiscoverFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val allChannelNotes = LocalCache.channels.values.mapNotNull { LocalCache.getNoteIfExists(it.idHex) }
|
||||
val allMessageNotes = LocalCache.channels.values.map { it.notes.values }.flatten()
|
||||
|
||||
val notes = innerApplyFilter(allChannelNotes + allMessageNotes)
|
||||
|
||||
return sort(notes)
|
||||
}
|
||||
|
||||
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||
return innerApplyFilter(collection)
|
||||
}
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val now = System.currentTimeMillis() / 1000
|
||||
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
|
||||
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||
|
||||
val activities = collection
|
||||
.asSequence()
|
||||
.filter { it.event is LiveActivitiesEvent }
|
||||
.filter { isGlobal || it.author?.pubkeyHex in followingKeySet }
|
||||
.filter { account.isAcceptable(it) }
|
||||
.filter { (it.createdAt() ?: 0) <= now }
|
||||
.toSet()
|
||||
|
||||
return activities
|
||||
}
|
||||
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
return collection.sortedWith(
|
||||
compareBy(
|
||||
{ convertStatusToOrder((it.event as? LiveActivitiesEvent)?.status()) },
|
||||
{ it.createdAt() },
|
||||
{ it.idHex }
|
||||
)
|
||||
).reversed()
|
||||
}
|
||||
|
||||
fun convertStatusToOrder(status: String?): Int {
|
||||
return when (status) {
|
||||
STATUS_LIVE -> 2
|
||||
STATUS_PLANNED -> 1
|
||||
STATUS_ENDED -> 0
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_LIVE
|
||||
import java.util.Date
|
||||
|
||||
class HomeLiveActivitiesFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
@@ -41,7 +42,7 @@ class HomeLiveActivitiesFeedFilter(val account: Account) : AdditiveFeedFilter<No
|
||||
.asSequence()
|
||||
.filter { it ->
|
||||
val noteEvent = it.event
|
||||
(noteEvent is LiveActivitiesEvent && noteEvent.createdAt > twoHrs && noteEvent.status() == "live" && OnlineChecker.isOnline(noteEvent.streaming())) &&
|
||||
(noteEvent is LiveActivitiesEvent && noteEvent.createdAt > twoHrs && noteEvent.status() == STATUS_LIVE && OnlineChecker.isOnline(noteEvent.streaming())) &&
|
||||
(isGlobal || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet)) &&
|
||||
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||
it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true
|
||||
|
||||
@@ -48,6 +48,7 @@ val bottomNavigationItems = listOf(
|
||||
Route.Home,
|
||||
Route.Message,
|
||||
Route.Video,
|
||||
Route.Discover,
|
||||
Route.Notification
|
||||
)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.navigation.compose.composable
|
||||
import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedLiveActivitiesViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
||||
@@ -22,6 +23,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.BookmarkListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DiscoverScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HiddenUsersScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HomeScreen
|
||||
@@ -42,6 +44,7 @@ fun AppNavigation(
|
||||
knownFeedViewModel: NostrChatroomListKnownFeedViewModel,
|
||||
newFeedViewModel: NostrChatroomListNewFeedViewModel,
|
||||
videoFeedViewModel: NostrVideoFeedViewModel,
|
||||
discoveryFeedViewModel: NostrDiscoverFeedViewModel,
|
||||
notifFeedViewModel: NotificationViewModel,
|
||||
userReactionsStatsModel: UserReactionsViewModel,
|
||||
|
||||
@@ -106,6 +109,16 @@ fun AppNavigation(
|
||||
})
|
||||
}
|
||||
|
||||
Route.Discover.let { route ->
|
||||
composable(route.route, route.arguments, content = {
|
||||
DiscoverScreen(
|
||||
discoveryFeedViewModel = discoveryFeedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
Route.Search.let { route ->
|
||||
composable(route.route, route.arguments, content = {
|
||||
SearchScreen(
|
||||
|
||||
@@ -118,6 +118,7 @@ private fun RenderTopRouteBar(
|
||||
// Route.Profile.route -> TopBarWithBackButton(nav)
|
||||
Route.Home.base -> HomeTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||
Route.Video.base -> StoriesTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||
Route.Discover.base -> DiscoveryTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||
Route.Notification.base -> NotificationTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||
else -> MainTopBar(scaffoldState, accountViewModel, nav)
|
||||
}
|
||||
@@ -178,6 +179,23 @@ fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: Scaffold
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DiscoveryTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
|
||||
val list by accountViewModel.accountLiveData.map {
|
||||
it.account.defaultDiscoveryFollowList
|
||||
}.observeAsState(GLOBAL_FOLLOWS)
|
||||
|
||||
FollowList(
|
||||
followLists,
|
||||
list,
|
||||
true
|
||||
) { listName ->
|
||||
accountViewModel.account.changeDefaultDiscoveryFollowList(listName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericTopBar(scaffoldState, accountViewModel, nav) {
|
||||
|
||||
@@ -57,6 +57,11 @@ sealed class Route(
|
||||
icon = R.drawable.ic_video
|
||||
)
|
||||
|
||||
object Discover : Route(
|
||||
route = "Discover",
|
||||
icon = R.drawable.ic_sensors
|
||||
)
|
||||
|
||||
object Notification : Route(
|
||||
route = "Notification",
|
||||
icon = R.drawable.ic_notifications,
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment.Companion.BottomStart
|
||||
import androidx.compose.ui.Alignment.Companion.TopEnd
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_ENDED
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_LIVE
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_PLANNED
|
||||
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.EndedFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ChannelCardCompose(
|
||||
baseNote: Note,
|
||||
routeForLastRead: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val isBlank by baseNote.live().metadata.map {
|
||||
it.note.event == null
|
||||
}.distinctUntilChanged().observeAsState(baseNote.event == null)
|
||||
|
||||
Crossfade(targetState = isBlank) {
|
||||
if (it) {
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
BlankNote(
|
||||
remember {
|
||||
modifier.combinedClickable(
|
||||
onClick = { },
|
||||
onLongClick = showPopup
|
||||
)
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
CheckHiddenChannelCardCompose(
|
||||
baseNote,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CheckHiddenChannelCardCompose(
|
||||
note: Note,
|
||||
routeForLastRead: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val isHidden by accountViewModel.accountLiveData.map {
|
||||
accountViewModel.isNoteHidden(note)
|
||||
}.distinctUntilChanged().observeAsState(accountViewModel.isNoteHidden(note))
|
||||
|
||||
Crossfade(targetState = isHidden) {
|
||||
if (!it) {
|
||||
LoadedChannelCardCompose(
|
||||
note,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadedChannelCardCompose(
|
||||
note: Note,
|
||||
routeForLastRead: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var state by remember {
|
||||
mutableStateOf(
|
||||
NoteComposeReportState(
|
||||
isAcceptable = true,
|
||||
canPreview = true,
|
||||
relevantReports = persistentSetOf()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
WatchForReports(note, accountViewModel) { newIsAcceptable, newCanPreview, newRelevantReports ->
|
||||
if (newIsAcceptable != state.isAcceptable || newCanPreview != state.canPreview) {
|
||||
val newState = NoteComposeReportState(newIsAcceptable, newCanPreview, newRelevantReports)
|
||||
scope.launch(Dispatchers.Main) {
|
||||
state = newState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Crossfade(targetState = state) {
|
||||
RenderChannelCardReportState(
|
||||
it,
|
||||
note,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderChannelCardReportState(
|
||||
state: NoteComposeReportState,
|
||||
note: Note,
|
||||
routeForLastRead: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var showReportedNote by remember { mutableStateOf(false) }
|
||||
|
||||
Crossfade(targetState = !state.isAcceptable && !showReportedNote) { showHiddenNote ->
|
||||
if (showHiddenNote) {
|
||||
HiddenNote(
|
||||
state.relevantReports,
|
||||
accountViewModel,
|
||||
modifier,
|
||||
false,
|
||||
nav,
|
||||
onClick = { showReportedNote = true }
|
||||
)
|
||||
} else {
|
||||
NormalChannelCard(
|
||||
note,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NormalChannelCard(
|
||||
baseNote: Note,
|
||||
routeForLastRead: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
CheckNewAndRenderChannelCard(
|
||||
baseNote,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
showPopup,
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CheckNewAndRenderChannelCard(
|
||||
baseNote: Note,
|
||||
routeForLastRead: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
showPopup: () -> Unit,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val newItemColor = MaterialTheme.colors.newItemBackgroundColor
|
||||
val defaultBackgroundColor = MaterialTheme.colors.background
|
||||
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
|
||||
|
||||
LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) {
|
||||
launch(Dispatchers.IO) {
|
||||
routeForLastRead?.let {
|
||||
val lastTime = accountViewModel.account.loadLastRead(it)
|
||||
|
||||
val createdAt = baseNote.createdAt()
|
||||
if (createdAt != null) {
|
||||
accountViewModel.account.markAsRead(it, createdAt)
|
||||
|
||||
val isNew = createdAt > lastTime
|
||||
|
||||
val newBackgroundColor = if (isNew) {
|
||||
if (parentBackgroundColor != null) {
|
||||
newItemColor.compositeOver(parentBackgroundColor.value)
|
||||
} else {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
}
|
||||
} else {
|
||||
parentBackgroundColor?.value ?: defaultBackgroundColor
|
||||
}
|
||||
|
||||
if (newBackgroundColor != backgroundColor.value) {
|
||||
launch(Dispatchers.Main) {
|
||||
backgroundColor.value = newBackgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: run {
|
||||
val newBackgroundColor = parentBackgroundColor?.value ?: defaultBackgroundColor
|
||||
|
||||
if (newBackgroundColor != backgroundColor.value) {
|
||||
launch(Dispatchers.Main) {
|
||||
backgroundColor.value = newBackgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClickableNote(
|
||||
baseNote = baseNote,
|
||||
backgroundColor = backgroundColor,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
showPopup = showPopup,
|
||||
nav = nav
|
||||
) {
|
||||
InnerChannelCardWithReactions(
|
||||
baseNote = baseNote,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InnerChannelCardWithReactions(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
Column() {
|
||||
RenderNoteRow(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderNoteRow(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
when (remember { baseNote.event }) {
|
||||
is LiveActivitiesEvent -> {
|
||||
RenderLiveActivityThumb(baseNote, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun RenderLiveActivityThumb(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val noteEvent = baseNote.event as? LiveActivitiesEvent ?: return
|
||||
|
||||
val eventUpdates by baseNote.live().metadata.observeAsState()
|
||||
|
||||
val media = remember(eventUpdates) { noteEvent.streaming() }
|
||||
val cover by remember(eventUpdates) {
|
||||
derivedStateOf {
|
||||
noteEvent.image()?.ifBlank { null }
|
||||
}
|
||||
}
|
||||
val subject = remember(eventUpdates) { noteEvent.title()?.ifBlank { null } }
|
||||
val content = remember(eventUpdates) { noteEvent.summary() }
|
||||
val participants = remember(eventUpdates) { noteEvent.participants() }
|
||||
val status = remember(eventUpdates) { noteEvent.status() }
|
||||
|
||||
var isOnline by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(key1 = media) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newIsOnline = OnlineChecker.isOnline(media)
|
||||
if (isOnline != newIsOnline) {
|
||||
isOnline = newIsOnline
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var participantUsers by remember {
|
||||
mutableStateOf<ImmutableList<User>>(
|
||||
persistentListOf()
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = eventUpdates) {
|
||||
launch(Dispatchers.IO) {
|
||||
val channel = LocalCache.getChannelIfExists(baseNote.idHex)
|
||||
val repliesByFollows = channel?.notes?.mapNotNull { it.value.author?.pubkeyHex }?.toSet() ?: emptySet()
|
||||
|
||||
val zappers = baseNote.publicZapAuthors()
|
||||
val likeAuthors = baseNote.reactionAuthors()
|
||||
|
||||
val allParticipants = (repliesByFollows + zappers + likeAuthors).filter { accountViewModel.isFollowing(it) }
|
||||
|
||||
val newParticipantUsers = (
|
||||
participants.mapNotNull { part ->
|
||||
if (part.key != baseNote.author?.pubkeyHex) {
|
||||
LocalCache.checkGetOrCreateUser(part.key)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} + allParticipants.mapNotNull {
|
||||
if (it != baseNote.author?.pubkeyHex) {
|
||||
LocalCache.checkGetOrCreateUser(it)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
).toImmutableList()
|
||||
|
||||
if (!equalImmutableLists(newParticipantUsers, participantUsers)) {
|
||||
participantUsers = newParticipantUsers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = TopEnd,
|
||||
modifier = Modifier
|
||||
.aspectRatio(ratio = 16f / 9f)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
cover?.let {
|
||||
AsyncImage(
|
||||
model = it,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(QuoteBorder)
|
||||
)
|
||||
} ?: run {
|
||||
baseNote.author?.let {
|
||||
DisplayAuthorBanner(it)
|
||||
}
|
||||
}
|
||||
|
||||
Box(Modifier.padding(10.dp)) {
|
||||
Crossfade(targetState = status) {
|
||||
when (it) {
|
||||
STATUS_LIVE -> {
|
||||
if (media.isNullOrBlank()) {
|
||||
LiveFlag()
|
||||
} else if (isOnline) {
|
||||
LiveFlag()
|
||||
} else {
|
||||
OfflineFlag()
|
||||
}
|
||||
}
|
||||
STATUS_ENDED -> {
|
||||
EndedFlag()
|
||||
}
|
||||
STATUS_PLANNED -> {
|
||||
ScheduledFlag()
|
||||
}
|
||||
else -> {
|
||||
EndedFlag()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(Modifier.padding(10.dp).align(BottomStart)) {
|
||||
if (participantUsers.isNotEmpty()) {
|
||||
FlowRow() {
|
||||
participantUsers.forEach {
|
||||
ClickableUserPicture(it, Size35dp, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChannelHeader(
|
||||
channelHex = remember { baseNote.idHex },
|
||||
showVideo = false,
|
||||
showBottomDiviser = false,
|
||||
showFlag = false,
|
||||
modifier = remember {
|
||||
Modifier.padding(start = 0.dp, end = 0.dp, top = 5.dp, bottom = 5.dp)
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayAuthorBanner(author: User) {
|
||||
val picture by author.live().metadata.map {
|
||||
it.user.info?.banner?.ifBlank { null } ?: it.user.info?.picture?.ifBlank { null }
|
||||
}.observeAsState()
|
||||
|
||||
AsyncImage(
|
||||
model = picture,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(QuoteBorder)
|
||||
)
|
||||
}
|
||||
@@ -150,19 +150,19 @@ private fun ChannelRoomCompose(
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val authorState by note.author!!.live().metadata.observeAsState()
|
||||
val authorName = remember(authorState) {
|
||||
val authorName = remember(note, authorState) {
|
||||
authorState?.user?.toBestDisplayName()
|
||||
}
|
||||
|
||||
val chanHex = remember { channel.idHex }
|
||||
|
||||
val channelState by channel.live.observeAsState()
|
||||
val channelPicture by remember(channelState) {
|
||||
val channelPicture by remember(note, channelState) {
|
||||
derivedStateOf {
|
||||
channel.profilePicture()
|
||||
}
|
||||
}
|
||||
val channelName by remember(channelState) {
|
||||
val channelName by remember(note, channelState) {
|
||||
derivedStateOf {
|
||||
channel.toBestDisplayName()
|
||||
}
|
||||
@@ -182,7 +182,7 @@ private fun ChannelRoomCompose(
|
||||
noteEvent?.content()
|
||||
}
|
||||
|
||||
var hasNewMessages = remember { mutableStateOf<Boolean>(false) }
|
||||
val hasNewMessages = remember { mutableStateOf<Boolean>(false) }
|
||||
|
||||
WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages ->
|
||||
if (hasNewMessages.value != newHasNewMessages) {
|
||||
|
||||
@@ -182,7 +182,7 @@ private fun NIP05VerifiedSymbol(nip05Verified: MutableState<Boolean?>) {
|
||||
)
|
||||
} else if (nip05Verified.value == true) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_verified),
|
||||
painter = painterResource(R.drawable.ic_verified_transparent),
|
||||
"NIP-05 Verified",
|
||||
tint = Nip05.copy(0.52f),
|
||||
modifier = Modifier
|
||||
@@ -218,7 +218,7 @@ fun DisplayNip05ProfileStatus(user: User) {
|
||||
)
|
||||
} else if (nip05Verified == true) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_verified),
|
||||
painter = painterResource(R.drawable.ic_verified_transparent),
|
||||
"NIP-05 Verified",
|
||||
tint = Nip05,
|
||||
modifier = Modifier.size(16.dp)
|
||||
|
||||
@@ -108,6 +108,8 @@ import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_LIVE
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_PLANNED
|
||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.Participant
|
||||
import com.vitorpamplona.amethyst.service.model.PeopleListEvent
|
||||
@@ -152,7 +154,6 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Following
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||
@@ -539,7 +540,7 @@ private fun CheckNewAndRenderNote(
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun ClickableNote(
|
||||
fun ClickableNote(
|
||||
baseNote: Note,
|
||||
modifier: Modifier,
|
||||
backgroundColor: MutableState<Color>,
|
||||
@@ -550,7 +551,7 @@ private fun ClickableNote(
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val updatedModifier = remember(backgroundColor.value) {
|
||||
val updatedModifier = remember(baseNote, backgroundColor.value) {
|
||||
modifier
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
@@ -1786,7 +1787,7 @@ private fun ReplyNoteComposition(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SecondUserInfoRow(
|
||||
fun SecondUserInfoRow(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -1810,7 +1811,7 @@ private fun SecondUserInfoRow(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FirstUserInfoRow(
|
||||
fun FirstUserInfoRow(
|
||||
baseNote: Note,
|
||||
showAuthorPicture: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -1855,7 +1856,7 @@ private fun BoostedMark() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MoreOptionsButton(
|
||||
fun MoreOptionsButton(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
@@ -2691,12 +2692,12 @@ fun RenderLiveActivityEventInner(baseNote: Note, accountViewModel: AccountViewMo
|
||||
|
||||
Crossfade(targetState = status) {
|
||||
when (it) {
|
||||
"live" -> {
|
||||
STATUS_LIVE -> {
|
||||
if (isOnline) {
|
||||
LiveFlag()
|
||||
}
|
||||
}
|
||||
"planned" -> {
|
||||
STATUS_PLANNED -> {
|
||||
ScheduledFlag()
|
||||
}
|
||||
}
|
||||
@@ -2745,7 +2746,7 @@ fun RenderLiveActivityEventInner(baseNote: Note, accountViewModel: AccountViewMo
|
||||
}
|
||||
|
||||
media?.let { media ->
|
||||
if (status == "live") {
|
||||
if (status == STATUS_LIVE) {
|
||||
if (isOnline) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -2842,7 +2843,7 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, accountView
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CreateImageHeader(
|
||||
fun CreateImageHeader(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
@@ -3220,10 +3221,10 @@ fun FollowingIcon(iconSize: Dp) {
|
||||
}
|
||||
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_verified),
|
||||
stringResource(id = R.string.following),
|
||||
painter = painterResource(R.drawable.verified_follow_shield),
|
||||
contentDescription = stringResource(id = R.string.following),
|
||||
modifier = modifier,
|
||||
tint = Following
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,5 +7,6 @@ fun ByteArray.toShortenHex(): String {
|
||||
}
|
||||
|
||||
fun String.toShortenHex(): String {
|
||||
if (length <= 16) return this
|
||||
return replaceRange(8, length - 8, ":")
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
@@ -33,20 +34,20 @@ import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@Composable
|
||||
fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier) {
|
||||
fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier, showPlayButton: Boolean = true) {
|
||||
val authorState by baseNote.live().metadata.map {
|
||||
it.note.author
|
||||
}.observeAsState(baseNote.author)
|
||||
|
||||
Crossfade(targetState = authorState, modifier = weight) {
|
||||
it?.let {
|
||||
UsernameDisplay(it, weight)
|
||||
UsernameDisplay(it, weight, showPlayButton)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier) {
|
||||
fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier, showPlayButton: Boolean = true) {
|
||||
val npubDisplay by remember {
|
||||
derivedStateOf {
|
||||
baseUser.pubkeyDisplayHex()
|
||||
@@ -59,7 +60,7 @@ fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier) {
|
||||
|
||||
Crossfade(targetState = userMetadata, modifier = weight) {
|
||||
if (it != null) {
|
||||
UserNameDisplay(it.bestUsername(), it.bestDisplayName(), npubDisplay, it.tags, weight)
|
||||
UserNameDisplay(it.bestUsername(), it.bestDisplayName(), npubDisplay, it.tags, weight, showPlayButton)
|
||||
} else {
|
||||
NPubDisplay(npubDisplay, weight)
|
||||
}
|
||||
@@ -72,14 +73,15 @@ private fun UserNameDisplay(
|
||||
bestDisplayName: String?,
|
||||
npubDisplay: String,
|
||||
tags: ImmutableListOfLists<String>?,
|
||||
modifier: Modifier
|
||||
modifier: Modifier,
|
||||
showPlayButton: Boolean = true
|
||||
) {
|
||||
if (bestUserName != null && bestDisplayName != null && bestDisplayName != bestUserName) {
|
||||
UserAndUsernameDisplay(bestDisplayName, tags, bestUserName, modifier)
|
||||
UserAndUsernameDisplay(bestDisplayName, tags, bestUserName, modifier, showPlayButton)
|
||||
} else if (bestDisplayName != null) {
|
||||
UserDisplay(bestDisplayName, tags, modifier)
|
||||
UserDisplay(bestDisplayName, tags, modifier, showPlayButton)
|
||||
} else if (bestUserName != null) {
|
||||
UserDisplay(bestUserName, tags, modifier)
|
||||
UserDisplay(bestUserName, tags, modifier, showPlayButton)
|
||||
} else {
|
||||
NPubDisplay(npubDisplay, modifier)
|
||||
}
|
||||
@@ -100,9 +102,10 @@ private fun NPubDisplay(npubDisplay: String, modifier: Modifier) {
|
||||
private fun UserDisplay(
|
||||
bestDisplayName: String,
|
||||
tags: ImmutableListOfLists<String>?,
|
||||
modifier: Modifier
|
||||
modifier: Modifier,
|
||||
showPlayButton: Boolean = true
|
||||
) {
|
||||
Row(modifier = modifier) {
|
||||
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
CreateTextWithEmoji(
|
||||
text = bestDisplayName,
|
||||
tags = tags,
|
||||
@@ -111,8 +114,10 @@ private fun UserDisplay(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = modifier
|
||||
)
|
||||
Spacer(StdHorzSpacer)
|
||||
DrawPlayName(bestDisplayName)
|
||||
if (showPlayButton) {
|
||||
Spacer(StdHorzSpacer)
|
||||
DrawPlayName(bestDisplayName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,9 +126,10 @@ private fun UserAndUsernameDisplay(
|
||||
bestDisplayName: String,
|
||||
tags: ImmutableListOfLists<String>?,
|
||||
bestUserName: String,
|
||||
modifier: Modifier
|
||||
modifier: Modifier,
|
||||
showPlayButton: Boolean = true
|
||||
) {
|
||||
Row(modifier = modifier) {
|
||||
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
CreateTextWithEmoji(
|
||||
text = bestDisplayName,
|
||||
tags = tags,
|
||||
@@ -138,8 +144,10 @@ private fun UserAndUsernameDisplay(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = modifier
|
||||
)
|
||||
Spacer(StdHorzSpacer)
|
||||
DrawPlayName(bestDisplayName)
|
||||
if (showPlayButton) {
|
||||
Spacer(StdHorzSpacer)
|
||||
DrawPlayName(bestDisplayName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.ChatroomFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.ChatroomListNewFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.DiscoverFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter
|
||||
@@ -65,6 +66,15 @@ class NostrVideoFeedViewModel(val account: Account) : FeedViewModel(VideoFeedFil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NostrDiscoverFeedViewModel(val account: Account) : FeedViewModel(DiscoverFeedFilter(account)) {
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <NostrDiscoverFeedViewModel : ViewModel> create(modelClass: Class<NostrDiscoverFeedViewModel>): NostrDiscoverFeedViewModel {
|
||||
return NostrDiscoverFeedViewModel(account) as NostrDiscoverFeedViewModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NostrThreadFeedViewModel(val noteId: String) : FeedViewModel(ThreadFeedFilter(noteId)) {
|
||||
class Factory(val noteId: String) : ViewModelProvider.Factory {
|
||||
override fun <NostrThreadFeedViewModel : ViewModel> create(modelClass: Class<NostrThreadFeedViewModel>): NostrThreadFeedViewModel {
|
||||
|
||||
@@ -17,6 +17,7 @@ object ScrollStateKeys {
|
||||
const val GLOBAL_SCREEN = "Global"
|
||||
const val NOTIFICATION_SCREEN = "Notifications"
|
||||
const val VIDEO_SCREEN = "Video"
|
||||
const val DISCOVER_SCREEN = "Discover"
|
||||
val HOME_FOLLOWS = Route.Home.base + "Follows"
|
||||
val HOME_REPLIES = Route.Home.base + "FollowsReplies"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountState
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
@@ -241,6 +242,10 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
return account.userProfile().isFollowingCached(user)
|
||||
}
|
||||
|
||||
fun isFollowing(user: HexKey): Boolean {
|
||||
return account.userProfile().isFollowingCached(user)
|
||||
}
|
||||
|
||||
val hideDeleteRequestDialog: Boolean
|
||||
get() = account.hideDeleteRequestDialog
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_LIVE
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewChannelView
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
||||
@@ -516,6 +517,7 @@ fun ChannelHeader(
|
||||
channelHex: String,
|
||||
showVideo: Boolean,
|
||||
showBottomDiviser: Boolean,
|
||||
showFlag: Boolean = true,
|
||||
modifier: Modifier = StdPadding,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -535,6 +537,7 @@ fun ChannelHeader(
|
||||
it,
|
||||
showVideo,
|
||||
showBottomDiviser,
|
||||
showFlag,
|
||||
modifier,
|
||||
accountViewModel,
|
||||
nav
|
||||
@@ -547,6 +550,7 @@ fun ChannelHeader(
|
||||
baseChannel: Channel,
|
||||
showVideo: Boolean,
|
||||
showBottomDiviser: Boolean,
|
||||
showFlag: Boolean = true,
|
||||
modifier: Modifier = StdPadding,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -564,27 +568,33 @@ fun ChannelHeader(
|
||||
val channelState by baseChannel.live.observeAsState()
|
||||
val channel = remember(channelState) { channelState?.channel } ?: return
|
||||
|
||||
val streamingUrl by remember(channelState) {
|
||||
derivedStateOf {
|
||||
val activity = channel as? LiveActivitiesChannel
|
||||
val description = activity?.info?.title()
|
||||
val url = activity?.info?.streaming()
|
||||
if (url != null) {
|
||||
ZoomableUrlVideo(url, description = description)
|
||||
} else {
|
||||
null
|
||||
if (showVideo) {
|
||||
val streamingUrl by remember(channelState) {
|
||||
derivedStateOf {
|
||||
val activity = channel as? LiveActivitiesChannel
|
||||
val description = activity?.info?.title()
|
||||
val url = activity?.info?.streaming()
|
||||
if (url != null) {
|
||||
ZoomableUrlVideo(url, description = description)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
streamingUrl?.let {
|
||||
CheckIfUrlIsOnline(it.url) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = remember { Modifier.heightIn(max = 300.dp) }
|
||||
) {
|
||||
ZoomableContentView(
|
||||
content = it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (streamingUrl != null && showVideo) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = remember { Modifier.heightIn(max = 300.dp) }) {
|
||||
ZoomableContentView(
|
||||
content = streamingUrl!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = modifier) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (channel is LiveActivitiesChannel) {
|
||||
@@ -653,7 +663,7 @@ fun ChannelHeader(
|
||||
ChannelActionOptions(channel, accountViewModel, nav)
|
||||
}
|
||||
if (channel is LiveActivitiesChannel) {
|
||||
LiveChannelActionOptions(channel, accountViewModel, nav)
|
||||
LiveChannelActionOptions(channel, showFlag, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -702,12 +712,13 @@ private fun ChannelActionOptions(
|
||||
@Composable
|
||||
private fun LiveChannelActionOptions(
|
||||
channel: LiveActivitiesChannel,
|
||||
showFlag: Boolean = true,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val isLive by remember(channel) {
|
||||
derivedStateOf {
|
||||
channel.info?.status() == "live"
|
||||
channel.info?.status() == STATUS_LIVE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,7 +727,7 @@ private fun LiveChannelActionOptions(
|
||||
}
|
||||
|
||||
note?.let {
|
||||
if (isLive) {
|
||||
if (showFlag && isLive) {
|
||||
LiveFlag()
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.service.NostrDiscoveryDataSource
|
||||
import com.vitorpamplona.amethyst.ui.note.ChannelCardCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedError
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.LoadingFeed
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
|
||||
import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
|
||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||
|
||||
@Composable
|
||||
fun DiscoverScreen(
|
||||
discoveryFeedViewModel: NostrDiscoverFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
WatchAccountForDiscoveryScreen(discoveryViewModel = discoveryFeedViewModel, accountViewModel = accountViewModel)
|
||||
|
||||
DisposableEffect(accountViewModel) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("Discovery Start")
|
||||
NostrDiscoveryDataSource.start()
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||
println("Discovery Stop")
|
||||
NostrDiscoveryDataSource.stop()
|
||||
}
|
||||
}
|
||||
|
||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifeCycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
RefresheableView(discoveryFeedViewModel, true) {
|
||||
SaveableFeedState(discoveryFeedViewModel, scrollStateKey = ScrollStateKeys.DISCOVER_SCREEN) { listState ->
|
||||
RenderDiscoverFeed(discoveryFeedViewModel, accountViewModel, listState, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderDiscoverFeed(
|
||||
viewModel: FeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
listState: LazyListState,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val feedState by viewModel.feedContent.collectAsState()
|
||||
|
||||
Crossfade(
|
||||
targetState = feedState,
|
||||
animationSpec = tween(durationMillis = 100)
|
||||
) { state ->
|
||||
when (state) {
|
||||
is FeedState.Empty -> {
|
||||
FeedEmpty {
|
||||
viewModel.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
is FeedState.FeedError -> {
|
||||
FeedError(state.errorMessage) {
|
||||
viewModel.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
is FeedState.Loaded -> {
|
||||
DiscoverFeedLoaded(
|
||||
state,
|
||||
listState,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
|
||||
is FeedState.Loading -> {
|
||||
LoadingFeed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchAccountForDiscoveryScreen(discoveryViewModel: NostrDiscoverFeedViewModel, accountViewModel: AccountViewModel) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
|
||||
LaunchedEffect(accountViewModel, accountState?.account?.defaultDiscoveryFollowList) {
|
||||
NostrDiscoveryDataSource.resetFilters()
|
||||
discoveryViewModel.checkKeysInvalidateDataAndSendToTop()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun DiscoverFeedLoaded(
|
||||
state: FeedState.Loaded,
|
||||
listState: LazyListState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(
|
||||
top = 10.dp,
|
||||
bottom = 10.dp
|
||||
),
|
||||
state = listState
|
||||
) {
|
||||
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
|
||||
val defaultModifier = remember {
|
||||
Modifier.padding(10.dp).animateItemPlacement()
|
||||
}
|
||||
|
||||
ChannelCardCompose(
|
||||
item,
|
||||
modifier = defaultModifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
||||
@@ -211,12 +212,22 @@ private fun FeedLoaded(
|
||||
|
||||
@Composable
|
||||
fun CheckIfLiveActivityIsOnline(note: Note, whenOnline: @Composable () -> Unit) {
|
||||
val noteState by note.live().metadata.observeAsState()
|
||||
val url by note.live().metadata.map {
|
||||
(note.event as? LiveActivitiesEvent)?.streaming()
|
||||
}.observeAsState()
|
||||
|
||||
url?.let {
|
||||
CheckIfUrlIsOnline(it, whenOnline)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CheckIfUrlIsOnline(url: String, whenOnline: @Composable () -> Unit) {
|
||||
var online by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(key1 = noteState) {
|
||||
LaunchedEffect(key1 = url) {
|
||||
launch(Dispatchers.IO) {
|
||||
online = OnlineChecker.isOnline((note.event as? LiveActivitiesEvent)?.streaming())
|
||||
online = OnlineChecker.isOnline(url)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.ui.screen.AccountState
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedLiveActivitiesViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
||||
@@ -98,6 +99,11 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
||||
factory = NostrVideoFeedViewModel.Factory(accountViewModel.account)
|
||||
)
|
||||
|
||||
val discoveryFeedViewModel: NostrDiscoverFeedViewModel = viewModel(
|
||||
key = accountViewModel.userProfile().pubkeyHex + "NostrDiscoveryFeedViewModel",
|
||||
factory = NostrDiscoverFeedViewModel.Factory(accountViewModel.account)
|
||||
)
|
||||
|
||||
val notifFeedViewModel: NotificationViewModel = viewModel(
|
||||
key = accountViewModel.userProfile().pubkeyHex + "NotificationViewModel",
|
||||
factory = NotificationViewModel.Factory(accountViewModel.account)
|
||||
@@ -136,6 +142,9 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
||||
Route.Video.base -> {
|
||||
videoFeedViewModel.sendToTop()
|
||||
}
|
||||
Route.Discover.base -> {
|
||||
discoveryFeedViewModel.sendToTop()
|
||||
}
|
||||
Route.Notification.base -> {
|
||||
notifFeedViewModel.invalidateDataAndSendToTop()
|
||||
}
|
||||
@@ -184,6 +193,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
||||
knownFeedViewModel,
|
||||
newFeedViewModel,
|
||||
videoFeedViewModel,
|
||||
discoveryFeedViewModel,
|
||||
notifFeedViewModel,
|
||||
userReactionsStatsModel,
|
||||
navController,
|
||||
|
||||
@@ -51,6 +51,7 @@ val Size17dp = 17.dp
|
||||
val Size18dp = 18.dp
|
||||
val Size19dp = 19.dp
|
||||
val Size20dp = 20.dp
|
||||
val Size22dp = 22.dp
|
||||
val Size24dp = 24.dp
|
||||
val Size25dp = 25.dp
|
||||
val Size30dp = 30.dp
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#9c9c9c"
|
||||
android:pathData="M142.11,817.89Q77.63,753.42 40.02,666.86 2.41,580.29 2.41,480q0,-100.29 37.61,-186.86 37.61,-86.56 102.08,-151.04l51.34,51.34q-54.92,54.92 -87.16,128.35 -32.24,73.43 -32.24,158.2 0,84.77 31.64,157.6 31.64,72.83 87.76,128.95zM277.03,682.97q-38.21,-38.21 -60.89,-90.14 -22.69,-51.94 -22.69,-112.83 0,-60.89 22.69,-112.83 22.69,-51.94 60.89,-90.14l51.34,51.34q-28.66,28.66 -45.97,67.46 -17.31,38.8 -17.31,84.17 0,45.37 16.72,83.58 16.72,38.21 46.56,68.06zM480,575.52q-39.4,0 -67.46,-28.06 -28.06,-28.06 -28.06,-67.46 0,-39.4 28.06,-67.46 28.06,-28.06 67.46,-28.06 39.4,0 67.46,28.06 28.06,28.06 28.06,67.46 0,39.4 -28.06,67.46 -28.06,28.06 -67.46,28.06zM682.97,682.97 L631.63,631.63q28.66,-28.66 45.97,-67.46 17.31,-38.8 17.31,-84.17 0,-45.37 -16.72,-83.58 -16.72,-38.21 -46.56,-68.06l51.34,-51.34q38.21,38.21 60.89,90.14 22.69,51.94 22.69,112.83 0,59.7 -22.69,111.64 -22.69,51.94 -60.89,91.34zM817.89,817.89 L766.55,766.55q54.92,-54.92 87.16,-128.35 32.24,-73.43 32.24,-158.2 0,-84.77 -31.64,-157.6 -31.64,-72.83 -87.76,-128.95l51.34,-51.34q64.47,65.67 102.08,152.23 37.61,86.56 37.61,185.66 0,99.1 -37.61,185.66 -37.61,86.56 -102.08,152.23z"
|
||||
android:strokeWidth="1.19396"/>
|
||||
</vector>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
@@ -23,7 +23,7 @@
|
||||
<string name="copy_note_id">குறிப்பின் IDஐ நகலெடுக்கவும்</string>
|
||||
<string name="broadcast">ஒளிபரப்பு</string>
|
||||
<string name="request_deletion">நீக்குவதற்கு கோரிக்கை செய்</string>
|
||||
<string name="block_hide_user">பயனரை முடக்கு + மறை</string>
|
||||
<string name="block_hide_user">யூஸரை முடக்கு + மறை</string>
|
||||
<string name="report_spam_scam">ஸ்பேம் / மோசடி என்று புகாரளிக்கவும்</string>
|
||||
<string name="report_impersonation">ஆள்மாறாட்டத்தைப் புகாரளிக்கவும்</string>
|
||||
<string name="report_explicit_content">வெளிப்படையான உள்ளடக்கத்தைப் புகாரளிக்கவும்</string>
|
||||
@@ -83,8 +83,8 @@
|
||||
<string name="add_a_relay">ஒரு ரிலேவை சேர்</string>
|
||||
<string name="display_name">காட்சிப் பெயர்</string>
|
||||
<string name="my_display_name">எனது காட்சிப் பெயர்</string>
|
||||
<string name="username">பயனர் பெயர்</string>
|
||||
<string name="my_username">எனது பயனர் பெயர்</string>
|
||||
<string name="username">யூஸர் பெயர்</string>
|
||||
<string name="my_username">எனது யூஸர் பெயர்</string>
|
||||
<string name="about_me">என்னைப் பற்றி</string>
|
||||
<string name="avatar_url">சுயவிவரப் படம் URL</string>
|
||||
<string name="banner_url">பேனர் URL</string>
|
||||
@@ -95,7 +95,7 @@
|
||||
<string name="failed_to_save_the_image">படத்தை சேமிப்பது தோல்வியடைந்தது</string>
|
||||
<string name="upload_image">படத்தை பதிவேற்று</string>
|
||||
<string name="uploading">பதிவேற்றம்...</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">பயனர் ஸாட்கள் பெறுவதற்கு லைட்னிங் முகவரி அமைக்கவில்லை</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">யூஸர் ஸாட்கள் பெறுவதற்கு லைட்னிங் முகவரி அமைக்கவில்லை</string>
|
||||
<string name="reply_here">"இங்கு பதிலளிக்கவும்.. "</string>
|
||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">குறிப்பு ID ஐ கிளிப்போர்டில் பிரதி எடுக்கும்</string>
|
||||
<string name="copy_channel_id_note_to_the_clipboard">அலைவரிசை ID ஐ கிளிப்போர்டில் பிரதி எடு</string>
|
||||
@@ -103,7 +103,7 @@
|
||||
<string name="join">சேர்</string>
|
||||
<string name="known">தெரிந்த</string>
|
||||
<string name="new_requests">புதிய கோரிக்கைகள்</string>
|
||||
<string name="blocked_users">முடக்கப்பட்ட பயனர்கள்</string>
|
||||
<string name="blocked_users">முடக்கப்பட்ட யூஸர்கள்</string>
|
||||
<string name="new_threads">புதிய நூல்கள்</string>
|
||||
<string name="conversations">உரையாடல்கள்</string>
|
||||
<string name="notes">குறிப்புகள்</string>
|
||||
@@ -119,13 +119,13 @@
|
||||
<string name="copies_the_public_key_to_the_clipboard_for_sharing">பகிர்வுக்காக கிளிப்போர்டுக்கு பொதுசாவியை நகலெடுக்கிறது</string>
|
||||
<string name="copy_public_key_npub_to_the_clipboard">கிளிப்போர்டுக்கு பொதுசாவியை (NPUB) நகலெடுக்கவும்</string>
|
||||
<string name="send_a_direct_message">நேரடி செய்தியை அனுப்பவும்</string>
|
||||
<string name="edits_the_user_s_metadata">பயனரின்தகவலை திருத்துகிறது</string>
|
||||
<string name="edits_the_user_s_metadata">யூஸரின்தகவலை திருத்துகிறது</string>
|
||||
<string name="follow">பின்தொடர்</string>
|
||||
<string name="follow_back">பதிலுக்கு பின்தொடரவும்</string>
|
||||
<string name="unblock">தடையை நீக்கு</string>
|
||||
<string name="copy_user_id">பயனர் IDயை நகலெடுக்கவும்</string>
|
||||
<string name="unblock_user">பயனரைத் தடைசெய்க</string>
|
||||
<string name="npub_hex_username">"npub, hex, பயனர்பெயர் "</string>
|
||||
<string name="copy_user_id">யூஸர் IDயை நகலெடுக்கவும்</string>
|
||||
<string name="unblock_user">யூஸர் தடையை நீக்கு</string>
|
||||
<string name="npub_hex_username">"npub, hex, யூஸர்பெயர் "</string>
|
||||
<string name="clear">அழி</string>
|
||||
<string name="app_logo">பயன்பாட்டு லோகோ</string>
|
||||
<string name="nsec_npub_hex_private_key">nsec / npub / hex தனிப்பட்ட சாவி</string>
|
||||
@@ -226,8 +226,8 @@
|
||||
<string name="report_dialog_impersonation">தீங்கிழைக்கும் ஆள்மாறாட்டம்</string>
|
||||
<string name="report_dialog_nudity">நிர்வாணம் அல்லது கிராஃபிக் உள்ளடக்கம்</string>
|
||||
<string name="report_dialog_illegal">சட்டவிரோத நடத்தை</string>
|
||||
<string name="report_dialog_blocking_a_user">ஒரு பயனரைத் தடுப்பது உங்கள் பயன்பாட்டில் அவர்களின் உள்ளடக்கத்தை மறைக்கும். நீங்கள் தடுக்கும் நபர்கள் உட்பட உங்கள் குறிப்புகள் இன்னும் பகிரங்கமாகக் காணப்படுகின்றன. தடுக்கப்பட்ட பயனர்கள் பாதுகாப்பு வடிப்பான்கள் திரையில் பட்டியலிடப்பட்டுள்ளனர்.</string>
|
||||
<string name="report_dialog_block_hide_user_btn">பயனரைத் தடுத்து மறைக்கவும்</string>
|
||||
<string name="report_dialog_blocking_a_user">ஒரு யூஸரைத் தடுப்பது உங்கள் பயன்பாட்டில் அவர்களின் உள்ளடக்கத்தை மறைக்கும். நீங்கள் தடுக்கும் நபர்கள் உட்பட உங்கள் குறிப்புகள் இன்னும் பகிரங்கமாகக் காணப்படுகின்றன. தடுக்கப்பட்ட யூஸர்கள் பாதுகாப்பு வடிப்பான்கள் திரையில் பட்டியலிடப்பட்டுள்ளனர்.</string>
|
||||
<string name="report_dialog_block_hide_user_btn">யூஸரைத் தடுத்து மறைக்கவும்</string>
|
||||
<string name="report_dialog_report_btn">தகாதது என பதிவுசெய்</string>
|
||||
<string name="report_dialog_reminder_public">இடுகையிடப்பட்ட அனைத்து அறிக்கைகளும் பகிரங்கமாக தெரியும்.</string>
|
||||
<string name="report_dialog_additional_reason_placeholder">உங்கள் அறிக்கையைப் பற்றிய கூடுதல் சூழலை விருப்பமாக வழங்கவும்…</string>
|
||||
@@ -267,7 +267,7 @@
|
||||
<string name="poll_closing_time_days">நாட்களில்</string>
|
||||
<string name="poll_is_closed">புதிய வாக்குகளுக்கு வாக்கெடுப்பு மூடப்பட்டுள்ளது</string>
|
||||
<string name="poll_zap_amount">ஜாப் தொகை</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">இந்த வகை வாக்கெடுப்பில் ஒரு பயனருக்கு ஒரு வாக்கு மட்டுமே அனுமதிக்கப்படுகிறது</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">இந்த வகை வாக்கெடுப்பில் ஒரு யூஸருக்கு ஒரு வாக்கு மட்டுமே அனுமதிக்கப்படுகிறது</string>
|
||||
<string name="looking_for_event">"நிகழ்வு %1$s ஐத் தேடி"</string>
|
||||
<string name="custom_zaps_add_a_message">ஒரு பொது செய்தியைச் சேர்க்கவும்</string>
|
||||
<string name="custom_zaps_add_a_message_private">ஒரு தனிப்பட்ட செய்தியைச் சேர்க்கவும்</string>
|
||||
@@ -283,4 +283,163 @@
|
||||
<string name="add_content">உருவாக்கி சேர்க்கவும்</string>
|
||||
<string name="content_description">உள்ளடக்கங்களின் விளக்கம்</string>
|
||||
<string name="content_description_example">சூரியன் மறையும் வேளையில் வெள்ளை மண் நிறைந்த கடற்கரையில் ஒரு நீலப் படகு</string>
|
||||
<string name="zap_type">ஜாப் டைப்</string>
|
||||
<string name="zap_type_explainer">அனைத்து வகைகளுக்கான ஜாப் டைப்</string>
|
||||
|
||||
<string name="zap_type_public">பொது</string>
|
||||
<string name="zap_type_public_explainer">அனைவரும் ஜாப் பரிவர்த்தனையையும் செய்தியையும் பார்க்கமுடியும்</string>
|
||||
|
||||
<string name="zap_type_private">தனிப்பட்ட</string>
|
||||
<string name="zap_type_private_explainer">பெறுபவரும் அனுப்புபவரும் மட்டும் ஒருவரை ஒருவர் மற்றும் ஜாப் செய்தியை பார்க்கமுடியும்</string>
|
||||
|
||||
<string name="zap_type_anonymous">பெயர் ரகசியமாக</string>
|
||||
<string name="zap_type_anonymous_explainer">பெறுபவருக்கும் மற்றவருக்கும் ஜாப் அனுப்புபவர் யார் என்று தெரியாது</string>
|
||||
|
||||
<string name="zap_type_nonzap">அல்லாத-ஜாப்</string>
|
||||
<string name="zap_type_nonzap_explainer">நாஸ்டரில் எந்த சுவடும் இன்றி, லைட்நிங்கில் மட்டும் உள்ளது</string>
|
||||
|
||||
|
||||
<string name="file_server">ஃபைல் சர்வர்</string>
|
||||
<string name="zap_forward_lnAddress">LN முகவரி அல்லது @யூஸர்</string>
|
||||
|
||||
<string name="upload_server_imgur">imgur.com - நம்பகமானது</string>
|
||||
<string name="upload_server_imgur_explainer">imgur ஃபைலை மாற்றமுடியும்</string>
|
||||
|
||||
<string name="upload_server_nostrimg">nostrimg.com - நம்பகமானது</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg ஃபைலை மாற்றமுடியும்</string>
|
||||
|
||||
<string name="upload_server_nostrbuild">nostr.build - நம்பகமானது</string>
|
||||
<string name="upload_server_nostrbuild_explainer">nostr.build ஃபைலை மாற்றமுடியும்</string>
|
||||
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - நம்பகமானது</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">nostrfiles.dev ஃபைலை மாற்றமுடியும்</string>
|
||||
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - நம்பகமானது</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me ஃபைலை மாற்றமுடியும்</string>
|
||||
|
||||
|
||||
<string name="upload_server_imgur_nip94">சரிபார்க்கக்கூடிய Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Imgur ஃபைலை மாற்றியிருக்கிறதா என்று சரிபார்க்கப்படும். புதிய NIP: பிற கிளயண்ட்டுகளில் இதை பார்க்க முடியாமல் இருக்கலாம்.</string>
|
||||
|
||||
<string name="upload_server_nostrimg_nip94">சரிபார்க்கக்கூடிய NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">NostrImg ஃபைலை மாற்றியிருக்கிறதா என்று சரிபார்க்கப்படும். புதிய NIP: பிற கிளயண்ட்டுகளில் இதை பார்க்க முடியாமல் இருக்கலாம்.</string>
|
||||
|
||||
<string name="upload_server_nostrbuild_nip94">சரிபார்க்கக்கூடிய Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Nostr.build ஃபைலை மாற்றியிருக்கிறதா என்று சரிபார்க்கப்படும். புதிய NIP: பிற கிளயண்ட்டுகளில் இதை பார்க்க முடியாமல் இருக்கலாம்.</string>
|
||||
|
||||
<string name="upload_server_nostrfilesdev_nip94">சரிபார்க்கக்கூடிய Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Nostrfiles.dev ஃபைலை மாற்றியிருக்கிறதா என்று சரிபார்க்கப்படும். புதிய NIP: பிற கிளயண்ட்டுகளில் இதை பார்க்க முடியாமல் இருக்கலாம்.</string>
|
||||
|
||||
<string name="upload_server_nostrcheckme_nip94">சரிபார்க்கக்கூடிய Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Nostrcheck.me ஃபைலை மாற்றியிருக்கிறதா என்று சரிபார்க்கப்படும். புதிய NIP: பிற கிளயண்ட்டுகளில் இதை பார்க்க முடியாமல் இருக்கலாம்.</string>
|
||||
|
||||
|
||||
|
||||
|
||||
<string name="upload_server_relays_nip95">உங்கள் ரிலே (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">ஃபைல்ஸ் உங்கள் ரிலேக்களில் ஹோஸ்ட் செய்யப்படும். புதிய NIP: ரிலேயில் இச்சேவை உள்ளதா என சரிபார்க்கவும்</string>
|
||||
|
||||
<string name="connect_via_tor_short">Tor/Orbot அமைப்பு முறை</string>
|
||||
<string name="connect_via_tor">Orbot அமைப்பு மூலம் இணைக்கவும்</string>
|
||||
|
||||
<string name="do_you_really_want_to_disable_tor_title">Tor/Orbot இணைப்பை துண்டிக்கவும்?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">உங்கள் டேட்டா உடனே வழக்கமான நெட்வொர்க்கிற்கு மாற்றப்படும்</string>
|
||||
<string name="yes">ஆம்</string>
|
||||
<string name="no">இல்லை</string>
|
||||
|
||||
|
||||
<string name="follow_list_selection">பின்பற்றப் படுவோர் பட்டியல்</string>
|
||||
<string name="follow_list_kind3follows">எல்லா பின்பற்றப் படுவோர்</string>
|
||||
<string name="follow_list_global">முழுதளாவிய</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown">
|
||||
## Tor மூலம் Orbot உடன் இணைக்க
|
||||
\n\n1. [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android) இன்ஸ்டால் செய்க
|
||||
\n2. Orbot-ஐ தொடங்குங்கள்
|
||||
\n3. Orbot-இல் Socks போர்ட்டை செக் செய்க. இயல்புநிலையில் அது 9050
|
||||
\n4. வேண்டுமானால் Orbot-இல் போர்ட்டை மாற்றுங்கள்
|
||||
\n5. இந்த திரையில் Socks போர்டை கட்டமைக்க
|
||||
\n6. ஆக்டிவேட் பொத்தானை அழுத்தி Orbot-ஐ proxy-ஆக பயன்படுத்துங்கள்
|
||||
</string>
|
||||
<string name="orbot_socks_port">Orbot Socks போர்ட்</string>
|
||||
<string name="invalid_port_number">செல்லாத போர்ட் நம்பர்</string>
|
||||
<string name="use_orbot">Orbot-ஐ பயன்படுத்துங்கள்</string>
|
||||
<string name="disconnect_from_your_orbot_setup">Orbot இணைப்பது துண்டிக்க</string>
|
||||
|
||||
<string name="app_notification_dms_channel_name">தனிப்பட்ட செய்திகள்</string>
|
||||
<string name="app_notification_dms_channel_description">தனிப்பட்ட செய்தி வருகையில் தெரிவிக்கவும்</string>
|
||||
|
||||
<string name="app_notification_zaps_channel_name">ஜாப் பெறப்பட்டது</string>
|
||||
<string name="app_notification_zaps_channel_description">யாரேனும் உங்களுக்கு ஜாப் அனுப்பும்போது தெரிவிக்கவும்</string>
|
||||
<string name="app_notification_zaps_channel_message">%1$s ஸாட்ஸ்</string>
|
||||
<string name="app_notification_zaps_channel_message_from">%1$s -இடமிருந்து</string>
|
||||
<string name="app_notification_zaps_channel_message_for">%1$s -க்கு</string>
|
||||
|
||||
<string name="reply_notify">தெரிவிக்க</string>
|
||||
|
||||
<string name="channel_list_join_conversation">உரையாடலில் இணைக</string>
|
||||
<string name="channel_list_user_or_group_id">யூஸர் அல்லது குழுவின் ID</string>
|
||||
<string name="channel_list_user_or_group_id_demo">npub, nevent அல்லது hex</string>
|
||||
<string name="channel_list_create_channel">உருவாக்குக</string>
|
||||
<string name="channel_list_join_channel">இணைக</string>
|
||||
|
||||
<string name="today">இன்று</string>
|
||||
|
||||
<string name="content_warning">உள்ளடக்க எச்சரிக்கை</string>
|
||||
<string name="content_warning_explanation">இந்த குறிப்பின் உள்ளடக்கம் சிலருக்கு அவமதிப்பாகவோ அருவெறுப்பாகவோ இருக்கலாம்</string>
|
||||
<string name="content_warning_hide_all_sensitive_content">எப்போதும் உணர்ச்சிபூர்வமான உள்ளடக்கத்தை மறைக்க</string>
|
||||
<string name="content_warning_show_all_sensitive_content">எப்போதும் உணர்ச்சிபூர்வமான உள்ளடக்கத்தை காண்பிக்க</string>
|
||||
<string name="content_warning_see_warnings">எப்போதும் உள்ளடக்க எச்சரிக்கையை காண்பிக்க</string>
|
||||
|
||||
<string name="recommended_apps">பரிந்துரைகள்</string>
|
||||
<string name="filter_spam_from_strangers">தெரியாதவர்களிடமிருந்து ஸ்பேம்-மை வடிகட்டுக</string>
|
||||
<string name="warn_when_posts_have_reports_from_your_follows">குறிப்புகளில் நீங்கள் பின்பற்றுபவரின் புகார் இருந்தால் எச்சரிக்க</string>
|
||||
|
||||
<string name="new_reaction_symbol">புதிய ரியாக்ஷன் குறி</string>
|
||||
<string name="no_reaction_type_setup_long_press_to_change">எந்த ரியாக்ஷனும் தேர்ந்தெடுக்கவில்லை. மாற்ற நீண்ட நேரம் அழுத்தவும்</string>
|
||||
|
||||
<string name="zapraiser">ஜாப்-திரட்டுதல்</string>
|
||||
<string name="zapraiser_explainer">இந்த குறிப்புடன் திரட்டுவதற்கான ஸாட்ஸ் இலக்குத் தொகை சேர்க்கப்படும். இப்பயன்பாட்டுள்ள கிளயண்ட்டுகள் ஜாப்-களை ஊக்குவிக்க ப்ரோக்ரேஸ் பார் மூலம் காண்பிக்கலாம்</string>
|
||||
<string name="zapraiser_target_amount_in_sats">ஸாட்ஸ் இலக்குத் தொகை</string>
|
||||
|
||||
<string name="sats_to_complete">ஜாப்-திரட்டுதல் %1$s. இலக்கை அடைய %2$s</string>
|
||||
<string name="read_from_relay">படிப்பதற்கான ரிலே</string>
|
||||
<string name="write_to_relay">போஸ்ட் செய்வதற்கான ரிலே</string>
|
||||
<string name="an_error_ocurred_trying_to_get_relay_information">%1$s ரிலே விவரங்கள் பெறுகையில் பிழை</string>
|
||||
<string name="owner">உரிமையாளர்</string>
|
||||
<string name="version">பதிப்பு</string>
|
||||
<string name="software">மென்பொருள்</string>
|
||||
<string name="contact">தொடர்புக்கு</string>
|
||||
<string name="supports">ஆதரிக்கப்படும் NIPகள்</string>
|
||||
<string name="admission_fees">அனுமதி கட்டணம்</string>
|
||||
<string name="payments_url">கட்டண URL</string>
|
||||
<string name="limitations">வரம்புகள்</string>
|
||||
<string name="countries">நாடுகள்</string>
|
||||
<string name="languages">மொழிகள்</string>
|
||||
<string name="tags">டாகுகள்</string>
|
||||
<string name="posting_policy">போஸ்ட் செய்வதற்கான கொள்கை</string>
|
||||
<string name="message_length">குறிப்பு நீளம்</string>
|
||||
<string name="subscriptions">சப்ஸ்க்ரிப்ஷன்கள்</string>
|
||||
<string name="filters">வடிப்பான்கள்</string>
|
||||
<string name="subscription_id_length">சப்ஸ்க்ரிப்ஷன் ID நீளம்</string>
|
||||
<string name="minimum_prefix">குறைந்தபட்ச முன்னீடு</string>
|
||||
<string name="maximum_event_tags">அதிகபட்ச நிகழ்வு டாகுகள்</string>
|
||||
<string name="content_length">உள்ளடக்க நீளம்</string>
|
||||
<string name="minimum_pow">குறைந்தபட்ச PoW</string>
|
||||
<string name="auth">சான்றளிப்பு</string>
|
||||
<string name="payment">கட்டணம்</string>
|
||||
<string name="cashu">Cashu டோக்கன்</string>
|
||||
<string name="cashu_redeem">மீட்க</string>
|
||||
|
||||
<string name="live_stream_live_tag">நேரலை</string>
|
||||
<string name="live_stream_offline_tag">ஆஃப்லைன்</string>
|
||||
<string name="live_stream_ended_tag">முடிந்தது</string>
|
||||
<string name="live_stream_planned_tag">வரவிருக்கிறது</string>
|
||||
|
||||
<string name="live_stream_is_offline">லைவ்ஸ்ட்ரீம் ஆஃப்லைனில் உள்ளது</string>
|
||||
<string name="live_stream_has_ended">லைவ்ஸ்ட்ரீம் முடிந்துவிட்டது</string>
|
||||
<string name="are_you_sure_you_want_to_log_out">வெளியேறு உங்கள் அனைத்து லோக்கல் விவரங்களையும் நீக்கிவிடும். வெளியேறும் முன் உங்கள் அக்கவுண்ட் தொலையாமல் இருக்க ரகசியசாவியை பத்திரமாக பிரதி செய்து சேமிக்கப்பட்டுள்ளதா என்று சரிபார்க்கவும். வெளியேற்றம் தொடர வேண்டுமா?</string>
|
||||
<string name="followed_tags">பின்பற்றப்படும் டாகுகள்</string>
|
||||
|
||||
<string name="relay_setup">ரிலேகள்</string>
|
||||
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user