mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-05 22:34:38 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3efd15b95 | ||
|
|
a1930c232b | ||
|
|
58a4fb18ae | ||
|
|
be4870da1a | ||
|
|
d2f6317d5c | ||
|
|
05fb408842 | ||
|
|
611305a406 | ||
|
|
5b856af19b | ||
|
|
ce5106684f | ||
|
|
f0fe2dfc17 | ||
|
|
fb5fad5d02 | ||
|
|
6314e3fcb2 | ||
|
|
da62760fd7 | ||
|
|
e3eae80d4c | ||
|
|
3cd0828cba | ||
|
|
806b86f950 | ||
|
|
c931e2cbee | ||
|
|
4e57eed5d8 | ||
|
|
8e725259eb | ||
|
|
b48d2df8d2 | ||
|
|
dbf34ae457 | ||
|
|
74be724835 | ||
|
|
012c7a7105 | ||
|
|
e50dff3b62 | ||
|
|
ad5a4d0b59 | ||
|
|
3116ea1ad9 | ||
|
|
72e474c9ec |
@@ -47,7 +47,7 @@ jobs:
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Build APK
|
||||
run: ./gradlew assembleRelease --stacktrace --no-daemon
|
||||
run: ./gradlew assembleRelease --stacktrace
|
||||
|
||||
- name: Sign APK (Google Play)
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 209
|
||||
versionName "0.58.2"
|
||||
versionCode 214
|
||||
versionName "0.60.1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@@ -64,6 +64,7 @@ private object PrefKeys {
|
||||
const val SHOW_SENSITIVE_CONTENT = "show_sensitive_content"
|
||||
const val WARN_ABOUT_REPORTS = "warn_about_reports"
|
||||
const val FILTER_SPAM_FROM_STRANGERS = "filter_spam_from_strangers"
|
||||
const val LAST_READ_PER_ROUTE = "last_read_route_per_route"
|
||||
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
|
||||
}
|
||||
|
||||
@@ -223,6 +224,7 @@ object LocalPreferences {
|
||||
putInt(PrefKeys.PROXY_PORT, account.proxyPort)
|
||||
putBoolean(PrefKeys.WARN_ABOUT_REPORTS, account.warnAboutPostsWithReports)
|
||||
putBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, account.filterSpamFromStrangers)
|
||||
putString(PrefKeys.LAST_READ_PER_ROUTE, gson.toJson(account.lastReadPerRoute))
|
||||
|
||||
if (account.showSensitiveContent == null) {
|
||||
remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
|
||||
@@ -320,6 +322,18 @@ object LocalPreferences {
|
||||
val filterSpam = getBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, true)
|
||||
val warnAboutReports = getBoolean(PrefKeys.WARN_ABOUT_REPORTS, true)
|
||||
|
||||
val lastReadPerRoute = try {
|
||||
getString(PrefKeys.LAST_READ_PER_ROUTE, null)?.let {
|
||||
gson.fromJson(
|
||||
it,
|
||||
object : TypeToken<Map<String, Long>>() {}.type
|
||||
) as Map<String, Long>
|
||||
} ?: mapOf()
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
mapOf()
|
||||
}
|
||||
|
||||
val a = Account(
|
||||
loggedIn = Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
|
||||
followingChannels = followingChannels,
|
||||
@@ -343,25 +357,14 @@ object LocalPreferences {
|
||||
proxyPort = proxyPort,
|
||||
showSensitiveContent = showSensitiveContent,
|
||||
warnAboutPostsWithReports = warnAboutReports,
|
||||
filterSpamFromStrangers = filterSpam
|
||||
filterSpamFromStrangers = filterSpam,
|
||||
lastReadPerRoute = lastReadPerRoute
|
||||
)
|
||||
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
fun saveLastRead(route: String, timestampInSecs: Long) {
|
||||
encryptedPreferences(currentAccount()).edit().apply {
|
||||
putLong(PrefKeys.LAST_READ(route), timestampInSecs)
|
||||
}.apply()
|
||||
}
|
||||
|
||||
fun loadLastRead(route: String): Long {
|
||||
encryptedPreferences(currentAccount()).run {
|
||||
return getLong(PrefKeys.LAST_READ(route), 0)
|
||||
}
|
||||
}
|
||||
|
||||
fun migrateSingleUserPrefs() {
|
||||
if (currentAccount() != null) return
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object NotificationCache {
|
||||
// TODO: This must be account-based
|
||||
val lastReadByRoute = mutableMapOf<String, Long>()
|
||||
|
||||
fun markAsRead(route: String, timestampInSecs: Long) {
|
||||
val lastTime = lastReadByRoute[route]
|
||||
if (lastTime == null || timestampInSecs > lastTime) {
|
||||
lastReadByRoute.put(route, timestampInSecs)
|
||||
|
||||
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
scope.launch {
|
||||
LocalPreferences.saveLastRead(route, timestampInSecs)
|
||||
live.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun load(route: String): Long {
|
||||
var lastTime = lastReadByRoute[route]
|
||||
if (lastTime == null) {
|
||||
lastTime = LocalPreferences.loadLastRead(route)
|
||||
lastReadByRoute[route] = lastTime
|
||||
}
|
||||
return lastTime
|
||||
}
|
||||
|
||||
// Observers line up here.
|
||||
val live: NotificationLiveData = NotificationLiveData(this)
|
||||
}
|
||||
|
||||
class NotificationLiveData(val cache: NotificationCache) : LiveData<NotificationState>(NotificationState(cache)) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(300, Dispatchers.IO)
|
||||
|
||||
fun invalidateData() {
|
||||
bundler.invalidate() {
|
||||
if (hasActiveObservers()) {
|
||||
postValue(NotificationState(cache))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationState(val cache: NotificationCache)
|
||||
@@ -70,13 +70,15 @@ class Account(
|
||||
var proxyPort: Int,
|
||||
var showSensitiveContent: Boolean? = null,
|
||||
var warnAboutPostsWithReports: Boolean = true,
|
||||
var filterSpamFromStrangers: Boolean = true
|
||||
var filterSpamFromStrangers: Boolean = true,
|
||||
var lastReadPerRoute: Map<String, Long> = mapOf<String, Long>()
|
||||
) {
|
||||
var transientHiddenUsers: Set<String> = setOf()
|
||||
|
||||
// Observers line up here.
|
||||
val live: AccountLiveData = AccountLiveData(this)
|
||||
val liveLanguages: AccountLiveData = AccountLiveData(this)
|
||||
val liveLastRead: AccountLiveData = AccountLiveData(this)
|
||||
val saveable: AccountLiveData = AccountLiveData(this)
|
||||
|
||||
var userProfileCache: User? = null
|
||||
@@ -329,9 +331,15 @@ class Account(
|
||||
}
|
||||
|
||||
note.event?.let {
|
||||
val event = RepostEvent.create(it, loggedIn.privKey!!)
|
||||
Client.send(event)
|
||||
LocalCache.consume(event)
|
||||
if (it.kind() == 1) {
|
||||
val event = RepostEvent.create(it, loggedIn.privKey!!)
|
||||
Client.send(event)
|
||||
LocalCache.consume(event)
|
||||
} else {
|
||||
val event = GenericRepostEvent.create(it, loggedIn.privKey!!)
|
||||
Client.send(event)
|
||||
LocalCache.consume(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,6 +507,7 @@ class Account(
|
||||
tags: List<String>? = null,
|
||||
zapReceiver: String? = null,
|
||||
wantsToMarkAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long? = null,
|
||||
replyingTo: String?,
|
||||
root: String?,
|
||||
directMentions: Set<HexKey>
|
||||
@@ -517,6 +526,7 @@ class Account(
|
||||
extraTags = tags,
|
||||
zapReceiver = zapReceiver,
|
||||
markAsSensitive = wantsToMarkAsSensitive,
|
||||
zapRaiserAmount = zapRaiserAmount,
|
||||
replyingTo = replyingTo,
|
||||
root = root,
|
||||
directMentions = directMentions,
|
||||
@@ -537,7 +547,8 @@ class Account(
|
||||
consensusThreshold: Int?,
|
||||
closedAt: Int?,
|
||||
zapReceiver: String? = null,
|
||||
wantsToMarkAsSensitive: Boolean
|
||||
wantsToMarkAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long? = null
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
@@ -557,14 +568,15 @@ class Account(
|
||||
consensusThreshold = consensusThreshold,
|
||||
closedAt = closedAt,
|
||||
zapReceiver = zapReceiver,
|
||||
markAsSensitive = wantsToMarkAsSensitive
|
||||
markAsSensitive = wantsToMarkAsSensitive,
|
||||
zapRaiserAmount = zapRaiserAmount
|
||||
)
|
||||
// println("Sending new PollNoteEvent: %s".format(signedEvent.toJson()))
|
||||
Client.send(signedEvent)
|
||||
LocalCache.consume(signedEvent)
|
||||
}
|
||||
|
||||
fun sendChannelMessage(message: String, toChannel: String, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean) {
|
||||
fun sendChannelMessage(message: String, toChannel: String, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
// val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
|
||||
@@ -578,13 +590,14 @@ class Account(
|
||||
mentions = mentionsHex,
|
||||
zapReceiver = zapReceiver,
|
||||
markAsSensitive = wantsToMarkAsSensitive,
|
||||
zapRaiserAmount = zapRaiserAmount,
|
||||
privateKey = loggedIn.privKey!!
|
||||
)
|
||||
Client.send(signedEvent)
|
||||
LocalCache.consume(signedEvent, null)
|
||||
}
|
||||
|
||||
fun sendPrivateMessage(message: String, toUser: User, replyingTo: Note? = null, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean) {
|
||||
fun sendPrivateMessage(message: String, toUser: User, replyingTo: Note? = null, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
|
||||
@@ -598,6 +611,7 @@ class Account(
|
||||
mentions = mentionsHex,
|
||||
zapReceiver = zapReceiver,
|
||||
markAsSensitive = wantsToMarkAsSensitive,
|
||||
zapRaiserAmount = zapRaiserAmount,
|
||||
privateKey = loggedIn.privKey!!,
|
||||
advertiseNip18 = false
|
||||
)
|
||||
@@ -1109,7 +1123,7 @@ class Account(
|
||||
return note.author?.let { isAcceptable(it) } ?: true && // if user hasn't hided this author
|
||||
isAcceptableDirect(note) &&
|
||||
(
|
||||
note.event !is RepostEvent ||
|
||||
(note.event !is RepostEvent && note.event !is GenericRepostEvent) ||
|
||||
(note.replyTo?.firstOrNull { isAcceptableDirect(it) } != null)
|
||||
) // is not a reaction about a blocked post
|
||||
}
|
||||
@@ -1117,7 +1131,7 @@ class Account(
|
||||
fun getRelevantReports(note: Note): Set<Note> {
|
||||
val followsPlusMe = userProfile().latestContactList?.verifiedFollowKeySetAndMe ?: emptySet()
|
||||
|
||||
val innerReports = if (note.event is RepostEvent) {
|
||||
val innerReports = if (note.event is RepostEvent || note.event is GenericRepostEvent) {
|
||||
note.replyTo?.map { getRelevantReports(it) }?.flatten() ?: emptyList()
|
||||
} else {
|
||||
emptyList()
|
||||
@@ -1154,6 +1168,19 @@ class Account(
|
||||
live.invalidateData()
|
||||
}
|
||||
|
||||
fun markAsRead(route: String, timestampInSecs: Long) {
|
||||
val lastTime = lastReadPerRoute[route]
|
||||
if (lastTime == null || timestampInSecs > lastTime) {
|
||||
lastReadPerRoute = lastReadPerRoute + Pair(route, timestampInSecs)
|
||||
saveable.invalidateData()
|
||||
liveLastRead.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun loadLastRead(route: String): Long {
|
||||
return lastReadPerRoute[route] ?: 0
|
||||
}
|
||||
|
||||
fun registerObservers() {
|
||||
// Observes relays to restart connections
|
||||
userProfile().live().relays.observeForever {
|
||||
|
||||
@@ -70,7 +70,14 @@ object LocalCache {
|
||||
return checkGetOrCreateAddressableNote(key)
|
||||
}
|
||||
if (isValidHexNpub(key)) {
|
||||
return getOrCreateNote(key)
|
||||
val note = getOrCreateNote(key)
|
||||
val noteEvent = note.event
|
||||
if (noteEvent is AddressableEvent) {
|
||||
// upgrade to the latest
|
||||
return checkGetOrCreateAddressableNote(noteEvent.address().toTag())
|
||||
} else {
|
||||
return note
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -179,9 +186,15 @@ object LocalCache {
|
||||
}
|
||||
|
||||
fun consume(event: PeopleListEvent) {
|
||||
val version = getOrCreateNote(event.id)
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (version.event == null) {
|
||||
version.loadEvent(event, author, emptyList())
|
||||
version.moveAllReferencesTo(note)
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event?.id() == event.id()) return
|
||||
|
||||
@@ -234,9 +247,15 @@ object LocalCache {
|
||||
}
|
||||
|
||||
fun consume(event: LongTextNoteEvent, relay: Relay?) {
|
||||
val version = getOrCreateNote(event.id)
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (version.event == null) {
|
||||
version.loadEvent(event, author, emptyList())
|
||||
version.moveAllReferencesTo(note)
|
||||
}
|
||||
|
||||
if (relay != null) {
|
||||
author.addRelayBeingUsed(relay, event.createdAt)
|
||||
note.addRelay(relay)
|
||||
@@ -299,10 +318,35 @@ object LocalCache {
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
private fun consume(event: PinListEvent) {
|
||||
private fun consume(event: LiveActivitiesEvent, relay: Relay?) {
|
||||
val version = getOrCreateNote(event.id)
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (version.event == null) {
|
||||
version.loadEvent(event, author, emptyList())
|
||||
version.moveAllReferencesTo(note)
|
||||
}
|
||||
|
||||
if (note.event?.id() == event.id()) return
|
||||
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
note.loadEvent(event, author, emptyList())
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
}
|
||||
|
||||
private fun consume(event: PinListEvent) {
|
||||
val version = getOrCreateNote(event.id)
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (version.event == null) {
|
||||
version.loadEvent(event, author, emptyList())
|
||||
version.moveAllReferencesTo(note)
|
||||
}
|
||||
|
||||
if (note.event?.id() == event.id()) return
|
||||
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
@@ -313,9 +357,15 @@ object LocalCache {
|
||||
}
|
||||
|
||||
private fun consume(event: RelaySetEvent) {
|
||||
val version = getOrCreateNote(event.id)
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (version.event == null) {
|
||||
version.loadEvent(event, author, emptyList())
|
||||
version.moveAllReferencesTo(note)
|
||||
}
|
||||
|
||||
if (note.event?.id() == event.id()) return
|
||||
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
@@ -326,9 +376,15 @@ object LocalCache {
|
||||
}
|
||||
|
||||
private fun consume(event: AudioTrackEvent) {
|
||||
val version = getOrCreateNote(event.id)
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (version.event == null) {
|
||||
version.loadEvent(event, author, emptyList())
|
||||
version.moveAllReferencesTo(note)
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event?.id() == event.id()) return
|
||||
|
||||
@@ -340,9 +396,15 @@ object LocalCache {
|
||||
}
|
||||
|
||||
fun consume(event: BadgeDefinitionEvent) {
|
||||
val version = getOrCreateNote(event.id)
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (version.event == null) {
|
||||
version.loadEvent(event, author, emptyList())
|
||||
version.moveAllReferencesTo(note)
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event?.id() == event.id()) return
|
||||
|
||||
@@ -354,9 +416,15 @@ object LocalCache {
|
||||
}
|
||||
|
||||
fun consume(event: BadgeProfilesEvent) {
|
||||
val version = getOrCreateNote(event.id)
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (version.event == null) {
|
||||
version.loadEvent(event, author, emptyList())
|
||||
version.moveAllReferencesTo(note)
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event?.id() == event.id()) return
|
||||
|
||||
@@ -392,21 +460,35 @@ object LocalCache {
|
||||
}
|
||||
|
||||
fun consume(event: AppDefinitionEvent) {
|
||||
val version = getOrCreateNote(event.id)
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (version.event == null) {
|
||||
version.loadEvent(event, author, emptyList())
|
||||
version.moveAllReferencesTo(note)
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event != null) return
|
||||
|
||||
note.loadEvent(event, author, emptyList())
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
note.loadEvent(event, author, emptyList())
|
||||
|
||||
refreshObservers(note)
|
||||
refreshObservers(note)
|
||||
}
|
||||
}
|
||||
|
||||
fun consume(event: AppRecommendationEvent) {
|
||||
val version = getOrCreateNote(event.id)
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (version.event == null) {
|
||||
version.loadEvent(event, author, emptyList())
|
||||
version.moveAllReferencesTo(note)
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event?.id() == event.id()) return
|
||||
|
||||
@@ -540,6 +622,31 @@ object LocalCache {
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
fun consume(event: GenericRepostEvent) {
|
||||
val note = getOrCreateNote(event.id)
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event != null) return
|
||||
|
||||
// Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
|
||||
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
// Prepares user's profile view.
|
||||
author.addNote(note)
|
||||
|
||||
// Counts the replies
|
||||
repliesTo.forEach {
|
||||
it.addBoost(note)
|
||||
}
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
fun consume(event: ReactionEvent) {
|
||||
val note = getOrCreateNote(event.id)
|
||||
|
||||
@@ -1064,6 +1171,7 @@ object LocalCache {
|
||||
is FileStorageEvent -> consume(event, relay)
|
||||
is FileStorageHeaderEvent -> consume(event, relay)
|
||||
is HighlightEvent -> consume(event, relay)
|
||||
is LiveActivitiesEvent -> consume(event, relay)
|
||||
is LnZapEvent -> {
|
||||
event.zapRequest?.let {
|
||||
verifyAndConsume(it, relay)
|
||||
@@ -1088,6 +1196,12 @@ object LocalCache {
|
||||
}
|
||||
consume(event)
|
||||
}
|
||||
is GenericRepostEvent -> {
|
||||
event.containedPost()?.let {
|
||||
verifyAndConsume(it, relay)
|
||||
}
|
||||
consume(event)
|
||||
}
|
||||
is TextNoteEvent -> consume(event, relay)
|
||||
is PollNoteEvent -> consume(event, relay)
|
||||
else -> {
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.vitorpamplona.amethyst.service.model.*
|
||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSETime
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.actions.updated
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import fr.acinq.secp256k1.Hex
|
||||
@@ -96,7 +97,7 @@ open class Note(val idHex: String) {
|
||||
*/
|
||||
fun replyLevelSignature(cachedSignatures: MutableMap<Note, String> = mutableMapOf()): String {
|
||||
val replyTo = replyTo
|
||||
if (replyTo == null || replyTo.isEmpty()) {
|
||||
if (event is RepostEvent || event is GenericRepostEvent || replyTo == null || replyTo.isEmpty()) {
|
||||
return "/" + formattedDateTime(createdAt() ?: 0) + ";"
|
||||
}
|
||||
|
||||
@@ -109,7 +110,7 @@ open class Note(val idHex: String) {
|
||||
|
||||
fun replyLevel(cachedLevels: MutableMap<Note, Int> = mutableMapOf()): Int {
|
||||
val replyTo = replyTo
|
||||
if (replyTo == null || replyTo.isEmpty()) {
|
||||
if (event is RepostEvent || event is GenericRepostEvent || replyTo == null || replyTo.isEmpty()) {
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -372,7 +373,7 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
|
||||
fun isNewThread(): Boolean {
|
||||
return event is RepostEvent || replyTo == null || replyTo?.size == 0
|
||||
return event is RepostEvent || event is GenericRepostEvent || replyTo == null || replyTo?.size == 0
|
||||
}
|
||||
|
||||
fun hasZapped(loggedIn: User): Boolean {
|
||||
@@ -400,6 +401,42 @@ open class Note(val idHex: String) {
|
||||
return boosts.filter { it.author == loggedIn }
|
||||
}
|
||||
|
||||
fun moveAllReferencesTo(note: AddressableNote) {
|
||||
// migrates these comments to a new version
|
||||
replies.forEach {
|
||||
note.addReply(it)
|
||||
it.replyTo = it.replyTo?.updated(this, note)
|
||||
}
|
||||
reactions.forEach {
|
||||
it.value.forEach {
|
||||
note.addReaction(it)
|
||||
it.replyTo = it.replyTo?.updated(this, note)
|
||||
}
|
||||
}
|
||||
boosts.forEach {
|
||||
note.addBoost(it)
|
||||
it.replyTo = it.replyTo?.updated(this, note)
|
||||
}
|
||||
reports.forEach {
|
||||
it.value.forEach {
|
||||
note.addReport(it)
|
||||
it.replyTo = it.replyTo?.updated(this, note)
|
||||
}
|
||||
}
|
||||
zaps.forEach {
|
||||
note.addZap(it.key, it.value)
|
||||
it.key.replyTo = it.key.replyTo?.updated(this, note)
|
||||
it.value?.replyTo = it.value?.replyTo?.updated(this, note)
|
||||
}
|
||||
|
||||
replyTo = null
|
||||
replies = emptySet()
|
||||
reactions = emptyMap()
|
||||
boosts = emptySet()
|
||||
reports = emptyMap()
|
||||
zaps = emptyMap()
|
||||
}
|
||||
|
||||
var liveSet: NoteLiveSet? = null
|
||||
|
||||
fun live(): NoteLiveSet {
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.vitorpamplona.amethyst.model
|
||||
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.model.ATag
|
||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@@ -10,6 +12,8 @@ class ThreadAssembler {
|
||||
private fun searchRoot(note: Note, testedNotes: MutableSet<Note> = mutableSetOf()): Note? {
|
||||
if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note
|
||||
|
||||
if (note.event is RepostEvent || note.event is GenericRepostEvent) return note
|
||||
|
||||
testedNotes.add(note)
|
||||
|
||||
val markedAsRoot = note.event?.tags()?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1)
|
||||
|
||||
@@ -96,10 +96,10 @@ class User(val pubkeyHex: String) {
|
||||
// Update Followers of the past user list
|
||||
// Update Followers of the new contact list
|
||||
(oldContactListEvent)?.unverifiedFollowKeySet()?.forEach {
|
||||
LocalCache.users[it]?.liveSet?.follows?.invalidateData()
|
||||
LocalCache.users[it]?.liveSet?.followers?.invalidateData()
|
||||
}
|
||||
(latestContactList)?.unverifiedFollowKeySet()?.forEach {
|
||||
LocalCache.users[it]?.liveSet?.follows?.invalidateData()
|
||||
LocalCache.users[it]?.liveSet?.followers?.invalidateData()
|
||||
}
|
||||
|
||||
liveSet?.relays?.invalidateData()
|
||||
@@ -340,6 +340,7 @@ class User(val pubkeyHex: String) {
|
||||
class UserLiveSet(u: User) {
|
||||
// UI Observers line up here.
|
||||
val follows: UserLiveData = UserLiveData(u)
|
||||
val followers: UserLiveData = UserLiveData(u)
|
||||
val reports: UserLiveData = UserLiveData(u)
|
||||
val messages: UserLiveData = UserLiveData(u)
|
||||
val relays: UserLiveData = UserLiveData(u)
|
||||
@@ -351,6 +352,7 @@ class UserLiveSet(u: User) {
|
||||
|
||||
fun isInUse(): Boolean {
|
||||
return follows.hasObservers() ||
|
||||
followers.hasObservers() ||
|
||||
reports.hasObservers() ||
|
||||
messages.hasObservers() ||
|
||||
relays.hasObservers() ||
|
||||
|
||||
@@ -88,6 +88,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
PollNoteEvent.kind,
|
||||
ReactionEvent.kind,
|
||||
RepostEvent.kind,
|
||||
GenericRepostEvent.kind,
|
||||
ReportEvent.kind,
|
||||
LnZapEvent.kind,
|
||||
LnZapPaymentResponseEvent.kind,
|
||||
|
||||
@@ -3,10 +3,12 @@ package com.vitorpamplona.amethyst.service
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PinListEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
@@ -58,7 +60,16 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS),
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, PinListEvent.kind),
|
||||
kinds = listOf(
|
||||
TextNoteEvent.kind,
|
||||
RepostEvent.kind,
|
||||
GenericRepostEvent.kind,
|
||||
LongTextNoteEvent.kind,
|
||||
PollNoteEvent.kind,
|
||||
HighlightEvent.kind,
|
||||
AudioTrackEvent.kind,
|
||||
PinListEvent.kind
|
||||
),
|
||||
authors = followSet,
|
||||
limit = 400,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList
|
||||
|
||||
@@ -26,7 +26,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(
|
||||
TextNoteEvent.kind, LongTextNoteEvent.kind,
|
||||
ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind,
|
||||
ReactionEvent.kind, RepostEvent.kind, GenericRepostEvent.kind, ReportEvent.kind,
|
||||
LnZapEvent.kind, LnZapRequestEvent.kind,
|
||||
BadgeAwardEvent.kind, BadgeDefinitionEvent.kind, BadgeProfilesEvent.kind,
|
||||
PollNoteEvent.kind, AudioTrackEvent.kind, PinListEvent.kind,
|
||||
@@ -77,6 +77,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
||||
LongTextNoteEvent.kind,
|
||||
ReactionEvent.kind,
|
||||
RepostEvent.kind,
|
||||
GenericRepostEvent.kind,
|
||||
ReportEvent.kind,
|
||||
LnZapEvent.kind,
|
||||
LnZapRequestEvent.kind,
|
||||
|
||||
@@ -28,7 +28,16 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(TextNoteEvent.kind, RepostEvent.kind, LongTextNoteEvent.kind, AudioTrackEvent.kind, PinListEvent.kind, PollNoteEvent.kind, HighlightEvent.kind),
|
||||
kinds = listOf(
|
||||
TextNoteEvent.kind,
|
||||
GenericRepostEvent.kind,
|
||||
RepostEvent.kind,
|
||||
LongTextNoteEvent.kind,
|
||||
AudioTrackEvent.kind,
|
||||
PinListEvent.kind,
|
||||
PollNoteEvent.kind,
|
||||
HighlightEvent.kind
|
||||
),
|
||||
authors = listOf(it.pubkeyHex),
|
||||
limit = 200
|
||||
)
|
||||
|
||||
@@ -35,7 +35,8 @@ class ChannelMessageEvent(
|
||||
zapReceiver: String?,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000,
|
||||
markAsSensitive: Boolean
|
||||
markAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long?
|
||||
): ChannelMessageEvent {
|
||||
val content = message
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
@@ -54,6 +55,9 @@ class ChannelMessageEvent(
|
||||
if (markAsSensitive) {
|
||||
tags.add(listOf("content-warning", ""))
|
||||
}
|
||||
zapRaiserAmount?.let {
|
||||
tags.add(listOf("zapraiser", "$it"))
|
||||
}
|
||||
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
|
||||
@@ -54,6 +54,10 @@ open class Event(
|
||||
(it.size > 1 && it[0] == "t" && it[1].equals("nude", true))
|
||||
}
|
||||
|
||||
override fun zapraiserAmount() = tags.firstOrNull() {
|
||||
(it.size > 1 && it[0].equals("zapraiser", true))
|
||||
}?.get(1)?.toLongOrNull()
|
||||
|
||||
override fun zapAddress() = tags.firstOrNull { it.size > 1 && it[0] == "zap" }?.get(1)
|
||||
|
||||
fun taggedAddresses() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull {
|
||||
@@ -245,7 +249,9 @@ open class Event(
|
||||
FileHeaderEvent.kind -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
FileStorageEvent.kind -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
FileStorageHeaderEvent.kind -> FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
GenericRepostEvent.kind -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
HighlightEvent.kind -> HighlightEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LiveActivitiesEvent.kind -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LnZapPaymentRequestEvent.kind -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LnZapPaymentResponseEvent.kind -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
@@ -38,4 +38,5 @@ interface EventInterface {
|
||||
|
||||
fun zapAddress(): String?
|
||||
fun isSensitive(): Boolean
|
||||
fun zapraiserAmount(): Long?
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class GenericRepostEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: List<List<String>>,
|
||||
content: String,
|
||||
sig: HexKey
|
||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
|
||||
fun boostedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
|
||||
fun containedPost() = try {
|
||||
fromJson(content, Client.lenient)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val kind = 16
|
||||
|
||||
fun create(boostedPost: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): GenericRepostEvent {
|
||||
val content = boostedPost.toJson()
|
||||
|
||||
val replyToPost = listOf("e", boostedPost.id())
|
||||
val replyToAuthor = listOf("p", boostedPost.pubKey())
|
||||
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
var tags: List<List<String>> = listOf(replyToPost, replyToAuthor)
|
||||
|
||||
if (boostedPost is AddressableEvent) {
|
||||
tags = tags + listOf(listOf("a", boostedPost.address().toTag()))
|
||||
}
|
||||
|
||||
tags = tags + listOf(listOf("k", "${boostedPost.kind()}"))
|
||||
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
return GenericRepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class LiveActivitiesEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: List<List<String>>,
|
||||
content: String,
|
||||
sig: HexKey
|
||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
|
||||
|
||||
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
|
||||
override fun address() = ATag(kind, pubKey, dTag(), null)
|
||||
|
||||
fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)
|
||||
fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1)
|
||||
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
|
||||
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 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)) }
|
||||
|
||||
companion object {
|
||||
const val kind = 30311
|
||||
|
||||
fun create(
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000
|
||||
): LiveActivitiesEvent {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, "")
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
return LiveActivitiesEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,8 @@ class PollNoteEvent(
|
||||
consensusThreshold: Int?,
|
||||
closedAt: Int?,
|
||||
zapReceiver: String?,
|
||||
markAsSensitive: Boolean
|
||||
markAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long?
|
||||
): PollNoteEvent {
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val tags = mutableListOf<List<String>>()
|
||||
@@ -79,6 +80,9 @@ class PollNoteEvent(
|
||||
if (markAsSensitive) {
|
||||
tags.add(listOf("content-warning", ""))
|
||||
}
|
||||
zapRaiserAmount?.let {
|
||||
tags.add(listOf("zapraiser", "$it"))
|
||||
}
|
||||
|
||||
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
|
||||
@@ -78,7 +78,8 @@ class PrivateDmEvent(
|
||||
createdAt: Long = Date().time / 1000,
|
||||
publishedRecipientPubKey: ByteArray? = null,
|
||||
advertiseNip18: Boolean = true,
|
||||
markAsSensitive: Boolean
|
||||
markAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long?
|
||||
): PrivateDmEvent {
|
||||
val content = Utils.encrypt(
|
||||
if (advertiseNip18) { nip18Advertisement } else { "" } + msg,
|
||||
@@ -102,6 +103,9 @@ class PrivateDmEvent(
|
||||
if (markAsSensitive) {
|
||||
tags.add(listOf("content-warning", ""))
|
||||
}
|
||||
zapRaiserAmount?.let {
|
||||
tags.add(listOf("zapraiser", "$it"))
|
||||
}
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
return PrivateDmEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||
|
||||
@@ -36,7 +36,7 @@ class RepostEvent(
|
||||
val replyToAuthor = listOf("p", boostedPost.pubKey())
|
||||
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
var tags: List<List<String>> = boostedPost.tags().plus(listOf(replyToPost, replyToAuthor))
|
||||
var tags: List<List<String>> = listOf(replyToPost, replyToAuthor)
|
||||
|
||||
if (boostedPost is AddressableEvent) {
|
||||
tags = tags + listOf(listOf("a", boostedPost.address().toTag()))
|
||||
|
||||
@@ -34,6 +34,7 @@ class TextNoteEvent(
|
||||
extraTags: List<String>?,
|
||||
zapReceiver: String?,
|
||||
markAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long?,
|
||||
replyingTo: String?,
|
||||
root: String?,
|
||||
directMentions: Set<HexKey>,
|
||||
@@ -89,6 +90,9 @@ class TextNoteEvent(
|
||||
if (markAsSensitive) {
|
||||
tags.add(listOf("content-warning", ""))
|
||||
}
|
||||
zapRaiserAmount?.let {
|
||||
tags.add(listOf("zapraiser", "$it"))
|
||||
}
|
||||
|
||||
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Created by Ayaan on 02/02/23, 10:30 pm
|
||||
* Copyright (c) 2023 . All rights reserved.
|
||||
* Last modified 02/02/23, 9:56 pm
|
||||
*/
|
||||
|
||||
package com.vitorpamplona.amethyst.service.tts
|
||||
|
||||
import android.content.Context
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.speech.tts.UtteranceProgressListener
|
||||
import java.util.Locale
|
||||
|
||||
const val DEF_SPEECH_AND_PITCH = 0.8f
|
||||
|
||||
fun getErrorText(errorCode: Int): String = when (errorCode) {
|
||||
TextToSpeech.ERROR -> "ERROR"
|
||||
TextToSpeech.ERROR_INVALID_REQUEST -> "ERROR_INVALID_REQUEST"
|
||||
TextToSpeech.ERROR_NETWORK -> "ERROR_NETWORK"
|
||||
TextToSpeech.ERROR_NETWORK_TIMEOUT -> "ERROR_NETWORK_TIMEOUT"
|
||||
TextToSpeech.ERROR_SERVICE -> "ERROR_SERVICE"
|
||||
TextToSpeech.ERROR_SYNTHESIS -> "ERROR_SYNTHESIS"
|
||||
TextToSpeech.ERROR_NOT_INSTALLED_YET -> "ERROR_NOT_INSTALLED_YET"
|
||||
else -> "UNKNOWN"
|
||||
}
|
||||
|
||||
class TextToSpeechEngine private constructor() {
|
||||
private var tts: TextToSpeech? = null
|
||||
|
||||
private var defaultPitch = 0.8f
|
||||
private var defaultSpeed = 0.8f
|
||||
private var defLanguage = Locale.getDefault()
|
||||
private var onStartListener: (() -> Unit)? = null
|
||||
private var onDoneListener: (() -> Unit)? = null
|
||||
private var onErrorListener: ((String) -> Unit)? = null
|
||||
private var onHighlightListener: ((Int, Int) -> Unit)? = null
|
||||
private var message: String? = null
|
||||
|
||||
companion object {
|
||||
private var instance: TextToSpeechEngine? = null
|
||||
fun getInstance(): TextToSpeechEngine {
|
||||
if (instance == null) {
|
||||
instance = TextToSpeechEngine()
|
||||
}
|
||||
return instance!!
|
||||
}
|
||||
}
|
||||
|
||||
fun initTTS(context: Context, message: String) {
|
||||
tts = TextToSpeech(context) {
|
||||
if (it == TextToSpeech.SUCCESS) {
|
||||
tts!!.language = defLanguage
|
||||
tts!!.setPitch(defaultPitch)
|
||||
tts!!.setSpeechRate(defaultSpeed)
|
||||
tts!!.setListener(
|
||||
onStart = {
|
||||
onStartListener?.invoke()
|
||||
},
|
||||
onError = { e ->
|
||||
e?.let { error ->
|
||||
onErrorListener?.invoke(error)
|
||||
}
|
||||
},
|
||||
onRange = { start, end ->
|
||||
if (this@TextToSpeechEngine.message != null) {
|
||||
onHighlightListener?.invoke(start, end)
|
||||
}
|
||||
},
|
||||
onDone = {
|
||||
onStartListener?.invoke()
|
||||
}
|
||||
)
|
||||
speak(message)
|
||||
} else {
|
||||
onErrorListener?.invoke(getErrorText(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun speak(message: String): TextToSpeechEngine {
|
||||
tts!!.speak(
|
||||
message,
|
||||
TextToSpeech.QUEUE_FLUSH,
|
||||
null,
|
||||
TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
fun setPitchAndSpeed(pitch: Float, speed: Float) {
|
||||
defaultPitch = pitch
|
||||
defaultSpeed = speed
|
||||
}
|
||||
|
||||
fun resetPitchAndSpeed() {
|
||||
defaultPitch = DEF_SPEECH_AND_PITCH
|
||||
defaultSpeed = DEF_SPEECH_AND_PITCH
|
||||
}
|
||||
|
||||
fun setLanguage(local: Locale): TextToSpeechEngine {
|
||||
this.defLanguage = local
|
||||
return this
|
||||
}
|
||||
|
||||
fun setHighlightedMessage(message: String) {
|
||||
this.message = message
|
||||
}
|
||||
|
||||
fun setOnStartListener(onStartListener: (() -> Unit)): TextToSpeechEngine {
|
||||
this.onStartListener = onStartListener
|
||||
return this
|
||||
}
|
||||
|
||||
fun setOnCompletionListener(onDoneListener: () -> Unit): TextToSpeechEngine {
|
||||
this.onDoneListener = onDoneListener
|
||||
return this
|
||||
}
|
||||
|
||||
fun setOnErrorListener(onErrorListener: (String) -> Unit): TextToSpeechEngine {
|
||||
this.onErrorListener = onErrorListener
|
||||
return this
|
||||
}
|
||||
|
||||
fun setOnHighlightListener(onHighlightListener: (Int, Int) -> Unit): TextToSpeechEngine {
|
||||
this.onHighlightListener = onHighlightListener
|
||||
return this
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
tts?.stop()
|
||||
tts?.shutdown()
|
||||
tts = null
|
||||
instance = null
|
||||
}
|
||||
}
|
||||
|
||||
inline fun TextToSpeech.setListener(
|
||||
crossinline onStart: (String?) -> Unit = {},
|
||||
crossinline onError: (String?) -> Unit = {},
|
||||
crossinline onRange: (Int, Int) -> Unit = { _, _ -> },
|
||||
crossinline onDone: (String?) -> Unit
|
||||
) = this.apply {
|
||||
setOnUtteranceProgressListener(object : UtteranceProgressListener() {
|
||||
override fun onStart(p0: String?) {
|
||||
onStart.invoke(p0)
|
||||
}
|
||||
|
||||
override fun onDone(p0: String?) {
|
||||
onDone.invoke(p0)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java", ReplaceWith("onError.invoke(p0)"))
|
||||
override fun onError(p0: String?) {
|
||||
onError.invoke(p0)
|
||||
}
|
||||
|
||||
override fun onRangeStart(utteranceId: String?, start: Int, end: Int, frame: Int) {
|
||||
super.onRangeStart(utteranceId, start, end, frame)
|
||||
onRange.invoke(start, end)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Created by Ayaan on 02/02/23, 10:30 pm
|
||||
* Copyright (c) 2023 . All rights reserved.
|
||||
* Last modified 02/02/23, 10:19 pm
|
||||
*/
|
||||
|
||||
package com.vitorpamplona.amethyst.service.tts
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.Locale
|
||||
|
||||
class TextToSpeechHelper private constructor(private val context: WeakReference<Context>) :
|
||||
LifecycleEventObserver {
|
||||
private val appContext
|
||||
get() = context.get()!!.applicationContext
|
||||
|
||||
private var message: String? = null
|
||||
|
||||
private var tts: TextToSpeechEngine? = null
|
||||
|
||||
private var onStart: (() -> Unit)? = null
|
||||
|
||||
private var onDoneListener: (() -> Unit)? = null
|
||||
|
||||
private var onErrorListener: ((String) -> Unit)? = null
|
||||
|
||||
private var onHighlightListener: ((Pair<Int, Int>) -> Unit)? = null
|
||||
|
||||
private var customActionForDestroy: (() -> Unit)? = null
|
||||
|
||||
init {
|
||||
initTTS()
|
||||
}
|
||||
|
||||
fun registerLifecycle(owner: LifecycleOwner): TextToSpeechHelper {
|
||||
owner.lifecycle.addObserver(this)
|
||||
return this
|
||||
}
|
||||
|
||||
private fun initTTS() = context.get()?.run {
|
||||
tts = TextToSpeechEngine.getInstance()
|
||||
.setOnCompletionListener { onDoneListener?.invoke() }
|
||||
.setOnErrorListener { onErrorListener?.invoke(it) }
|
||||
.setOnStartListener { onStart?.invoke() }
|
||||
}
|
||||
|
||||
fun speak(message: String): TextToSpeechHelper {
|
||||
if (tts == null) {
|
||||
initTTS()
|
||||
}
|
||||
this.message = message
|
||||
|
||||
tts?.initTTS(
|
||||
appContext,
|
||||
message
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will highlight the text in the textView
|
||||
*
|
||||
* @exception Exception("Message can't be null for highlighting !! Call speak() first")
|
||||
*/
|
||||
fun highlight(): TextToSpeechHelper {
|
||||
if (message == null) throw Exception("Message can't be null for highlighting !! Call speak() first")
|
||||
tts?.setHighlightedMessage(message!!)
|
||||
tts?.setOnHighlightListener { i, i2 ->
|
||||
onHighlightListener?.invoke(Pair(i, i2))
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun removeHighlight(): TextToSpeechHelper {
|
||||
message = null
|
||||
onHighlightListener = null
|
||||
return this
|
||||
}
|
||||
|
||||
fun destroy(
|
||||
action: (() -> Unit) = {}
|
||||
) {
|
||||
tts?.destroy()
|
||||
tts = null
|
||||
action.invoke()
|
||||
INSTANCE = null
|
||||
}
|
||||
|
||||
fun onStart(onStartListener: () -> Unit): TextToSpeechHelper {
|
||||
this.onStart = onStartListener
|
||||
return this
|
||||
}
|
||||
|
||||
fun onDone(onCompleteListener: () -> Unit): TextToSpeechHelper {
|
||||
this.onDoneListener = onCompleteListener
|
||||
return this
|
||||
}
|
||||
|
||||
fun onError(onErrorListener: (String) -> Unit): TextToSpeechHelper {
|
||||
this.onErrorListener = onErrorListener
|
||||
return this
|
||||
}
|
||||
|
||||
fun onHighlight(onHighlightListener: (Pair<Int, Int>) -> Unit): TextToSpeechHelper {
|
||||
this.onHighlightListener = onHighlightListener
|
||||
return this
|
||||
}
|
||||
|
||||
fun setCustomActionForDestroy(action: () -> Unit): TextToSpeechHelper {
|
||||
customActionForDestroy = action
|
||||
return this
|
||||
}
|
||||
|
||||
fun setLanguage(locale: Locale): TextToSpeechHelper {
|
||||
tts?.setLanguage(locale)
|
||||
return this
|
||||
}
|
||||
|
||||
fun setPitchAndSpeed(
|
||||
pitch: Float = DEF_SPEECH_AND_PITCH,
|
||||
speed: Float = DEF_SPEECH_AND_PITCH
|
||||
): TextToSpeechHelper {
|
||||
tts?.setPitchAndSpeed(pitch, speed)
|
||||
return this
|
||||
}
|
||||
|
||||
fun resetPitchAndSpeed(): TextToSpeechHelper {
|
||||
tts?.resetPitchAndSpeed()
|
||||
return this
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var INSTANCE: TextToSpeechHelper? = null
|
||||
fun getInstance(context: Context): TextToSpeechHelper {
|
||||
synchronized(TextToSpeechHelper::class.java) {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = TextToSpeechHelper(WeakReference(context))
|
||||
}
|
||||
return INSTANCE!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||
if (event == Lifecycle.Event.ON_DESTROY ||
|
||||
event == Lifecycle.Event.ON_STOP ||
|
||||
event == Lifecycle.Event.ON_PAUSE
|
||||
) {
|
||||
destroy {
|
||||
customActionForDestroy?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.material.icons.filled.ArrowForwardIos
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material.icons.filled.CurrencyBitcoin
|
||||
import androidx.compose.material.icons.filled.ShowChart
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material.icons.outlined.ArrowForwardIos
|
||||
@@ -279,6 +280,15 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
||||
}
|
||||
}
|
||||
|
||||
if (lud16 != null && postViewModel.wantsZapraiser) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) {
|
||||
ZapRaiserRequest(
|
||||
stringResource(id = R.string.zapraiser),
|
||||
postViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val myUrlPreview = postViewModel.urlPreview
|
||||
if (myUrlPreview != null) {
|
||||
Row(modifier = Modifier.padding(top = 5.dp)) {
|
||||
@@ -374,6 +384,12 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
||||
}
|
||||
}
|
||||
|
||||
if (postViewModel.canAddZapRaiser) {
|
||||
AddZapraiserButton(postViewModel.wantsZapraiser) {
|
||||
postViewModel.wantsZapraiser = !postViewModel.wantsZapraiser
|
||||
}
|
||||
}
|
||||
|
||||
MarkAsSensitive(postViewModel) {
|
||||
postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive
|
||||
}
|
||||
@@ -460,6 +476,56 @@ private fun AddPollButton(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddZapraiserButton(
|
||||
isLnInvoiceActive: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onClick()
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.height(20.dp)
|
||||
.width(25.dp)
|
||||
) {
|
||||
if (!isLnInvoiceActive) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ShowChart,
|
||||
null,
|
||||
modifier = Modifier.size(20.dp).align(Alignment.TopStart),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(R.string.zaps),
|
||||
modifier = Modifier
|
||||
.size(13.dp)
|
||||
.align(Alignment.BottomEnd),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ShowChart,
|
||||
null,
|
||||
modifier = Modifier.size(20.dp).align(Alignment.TopStart),
|
||||
tint = BitcoinOrange
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(R.string.zaps),
|
||||
modifier = Modifier
|
||||
.size(13.dp)
|
||||
.align(Alignment.BottomEnd),
|
||||
tint = BitcoinOrange
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddLnInvoiceButton(
|
||||
isLnInvoiceActive: Boolean,
|
||||
|
||||
@@ -74,6 +74,11 @@ open class NewPostViewModel() : ViewModel() {
|
||||
// NSFW, Sensitive
|
||||
var wantsToMarkAsSensitive by mutableStateOf(false)
|
||||
|
||||
// ZapRaiser
|
||||
var canAddZapRaiser by mutableStateOf(false)
|
||||
var wantsZapraiser by mutableStateOf(false)
|
||||
var zapRaiserAmount by mutableStateOf<Long?>(null)
|
||||
|
||||
open fun load(account: Account, replyingTo: Note?, quote: Note?) {
|
||||
originalNote = replyingTo
|
||||
replyingTo?.let { replyNote ->
|
||||
@@ -105,11 +110,14 @@ open class NewPostViewModel() : ViewModel() {
|
||||
}
|
||||
|
||||
canAddInvoice = account.userProfile().info?.lnAddress() != null
|
||||
canAddZapRaiser = account.userProfile().info?.lnAddress() != null
|
||||
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null
|
||||
contentToAddUrl = null
|
||||
|
||||
wantsForwardZapTo = false
|
||||
wantsToMarkAsSensitive = false
|
||||
wantsZapraiser = false
|
||||
zapRaiserAmount = null
|
||||
forwardZapTo = null
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
|
||||
@@ -130,12 +138,14 @@ open class NewPostViewModel() : ViewModel() {
|
||||
null
|
||||
}
|
||||
|
||||
val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount else null
|
||||
|
||||
if (wantsPoll) {
|
||||
account?.sendPoll(tagger.message, tagger.replyTos, tagger.mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt, zapReceiver, wantsToMarkAsSensitive)
|
||||
account?.sendPoll(tagger.message, tagger.replyTos, tagger.mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount)
|
||||
} else if (originalNote?.channelHex() != null) {
|
||||
account?.sendChannelMessage(tagger.message, tagger.channelHex!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive)
|
||||
account?.sendChannelMessage(tagger.message, tagger.channelHex!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount)
|
||||
} else if (originalNote?.event is PrivateDmEvent) {
|
||||
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive)
|
||||
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount)
|
||||
} else {
|
||||
// adds markers
|
||||
val rootId =
|
||||
@@ -151,6 +161,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
tags = null,
|
||||
zapReceiver = zapReceiver,
|
||||
wantsToMarkAsSensitive = wantsToMarkAsSensitive,
|
||||
zapRaiserAmount = localZapRaiserAmount,
|
||||
replyingTo = replyId,
|
||||
root = rootId,
|
||||
directMentions = tagger.directMentions
|
||||
@@ -228,6 +239,8 @@ open class NewPostViewModel() : ViewModel() {
|
||||
closedAt = null
|
||||
|
||||
wantsInvoice = false
|
||||
wantsZapraiser = false
|
||||
zapRaiserAmount = null
|
||||
|
||||
wantsForwardZapTo = false
|
||||
wantsToMarkAsSensitive = false
|
||||
@@ -333,8 +346,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
}
|
||||
|
||||
fun canPost(): Boolean {
|
||||
return message.text.isNotBlank() && !isUploadingImage && !wantsInvoice &&
|
||||
(!wantsPoll || pollOptions.values.all { it.isNotEmpty() }) && contentToAddUrl == null
|
||||
return message.text.isNotBlank() && !isUploadingImage && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount != null) && (!wantsPoll || pollOptions.values.all { it.isNotEmpty() }) && contentToAddUrl == null
|
||||
}
|
||||
|
||||
fun includePollHashtagInMessage(include: Boolean, hashtag: String) {
|
||||
|
||||
@@ -52,6 +52,7 @@ import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import java.lang.Math.round
|
||||
|
||||
@@ -477,7 +478,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni
|
||||
imageVector = Icons.Default.Download,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(35.dp)
|
||||
.size(Size35dp)
|
||||
.padding(horizontal = 5.dp),
|
||||
tint = if (read) Color.Green else MaterialTheme.colors.placeholderText
|
||||
)
|
||||
@@ -488,7 +489,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni
|
||||
imageVector = Icons.Default.Upload,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(35.dp)
|
||||
.size(Size35dp)
|
||||
.padding(horizontal = 5.dp),
|
||||
tint = if (write) Color.Green else MaterialTheme.colors.placeholderText
|
||||
)
|
||||
|
||||
@@ -67,7 +67,7 @@ import java.net.URL
|
||||
import java.util.regex.Pattern
|
||||
|
||||
val imageExtensions = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg")
|
||||
val videoExtensions = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov", "mp3")
|
||||
val videoExtensions = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov", "mp3", "m3u8")
|
||||
|
||||
// Group 1 = url, group 4 additional chars
|
||||
val noProtocolUrlValidator = Pattern.compile("(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+)*\\/?)(.*)")
|
||||
|
||||
@@ -53,6 +53,7 @@ import com.google.android.exoplayer2.ExoPlayer
|
||||
import com.google.android.exoplayer2.MediaItem
|
||||
import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerView
|
||||
import com.vitorpamplona.amethyst.VideoCache
|
||||
@@ -101,6 +102,7 @@ fun VideoView(
|
||||
onDialog: ((Boolean) -> Unit)? = null
|
||||
) {
|
||||
var exoPlayerData by remember { mutableStateOf<VideoPlayer?>(null) }
|
||||
val defaultToStart by remember { mutableStateOf(DefaultMutedSetting.value) }
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(key1 = videoUri) {
|
||||
@@ -112,7 +114,7 @@ fun VideoView(
|
||||
}
|
||||
|
||||
exoPlayerData?.let {
|
||||
VideoView(videoUri, description, it, thumb, onDialog)
|
||||
VideoView(videoUri, description, it, defaultToStart, thumb, onDialog)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
@@ -127,6 +129,7 @@ fun VideoView(
|
||||
videoUri: String,
|
||||
description: String? = null,
|
||||
exoPlayerData: VideoPlayer,
|
||||
defaultToStart: Boolean = false,
|
||||
thumb: VideoThumb? = null,
|
||||
onDialog: ((Boolean) -> Unit)? = null
|
||||
) {
|
||||
@@ -137,9 +140,15 @@ fun VideoView(
|
||||
exoPlayerData.exoPlayer.apply {
|
||||
repeatMode = Player.REPEAT_MODE_ALL
|
||||
videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT
|
||||
volume = if (DefaultMutedSetting.value) 0f else 1f
|
||||
if (videoUri.startsWith("file") == true) {
|
||||
volume = if (defaultToStart) 0f else 1f
|
||||
if (videoUri.startsWith("file")) {
|
||||
setMediaItem(media)
|
||||
} else if (videoUri.endsWith("m3u8")) {
|
||||
setMediaSource(
|
||||
HlsMediaSource.Factory(VideoCache.get()).createMediaSource(
|
||||
media
|
||||
)
|
||||
)
|
||||
} else {
|
||||
setMediaSource(
|
||||
ProgressiveMediaSource.Factory(VideoCache.get()).createMediaSource(
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@Composable
|
||||
fun ZapRaiserRequest(
|
||||
titleText: String? = null,
|
||||
newPostViewModel: NewPostViewModel
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 10.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.lightning),
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
|
||||
Text(
|
||||
text = titleText ?: stringResource(R.string.zapraiser),
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.W500,
|
||||
modifier = Modifier.padding(start = 10.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.zapraiser_explainer),
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
modifier = Modifier.padding(vertical = 10.dp)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringResource(R.string.zapraiser_target_amount_in_sats)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = if (newPostViewModel.zapRaiserAmount != null) {
|
||||
newPostViewModel.zapRaiserAmount.toString()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
onValueChange = {
|
||||
runCatching {
|
||||
if (it.isEmpty()) {
|
||||
newPostViewModel.zapRaiserAmount = null
|
||||
} else {
|
||||
newPostViewModel.zapRaiserAmount = it.toLongOrNull()
|
||||
}
|
||||
}
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "1000",
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Number
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||
@@ -26,14 +27,24 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
|
||||
val oneHr = 60 * 60
|
||||
|
||||
return collection
|
||||
.asSequence()
|
||||
.filter { it ->
|
||||
(it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent || it.event is PollNoteEvent || it.event is HighlightEvent || it.event is AudioTrackEvent) &&
|
||||
(it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) &&
|
||||
val noteEvent = it.event
|
||||
(noteEvent is TextNoteEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent || noteEvent is LongTextNoteEvent || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || noteEvent is AudioTrackEvent) &&
|
||||
(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 &&
|
||||
it.isNewThread()
|
||||
it.isNewThread() &&
|
||||
(
|
||||
(noteEvent !is RepostEvent && noteEvent !is GenericRepostEvent) || // not a repost
|
||||
(
|
||||
it.replyTo?.lastOrNull()?.author?.pubkeyHex !in followingKeySet ||
|
||||
(noteEvent.createdAt() > (it.replyTo?.lastOrNull()?.createdAt() ?: 0) + oneHr)
|
||||
) // or a repost of by a non-follower's post (likely not seen yet)
|
||||
)
|
||||
}
|
||||
.toSet()
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
||||
return note.replyTo?.lastOrNull()?.author?.pubkeyHex == authorHex
|
||||
}
|
||||
|
||||
if (event is RepostEvent) {
|
||||
if (event is RepostEvent || event is GenericRepostEvent) {
|
||||
return note.replyTo?.lastOrNull()?.author?.pubkeyHex == authorHex
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ class UserProfileAppRecommendationsFeedFilter(val user: User) : FeedFilter<Note>
|
||||
}.mapNotNull {
|
||||
(it.event as? AppRecommendationEvent)?.recommendations()
|
||||
}.flatten()
|
||||
.mapNotNull {
|
||||
.map {
|
||||
LocalCache.getOrCreateAddressableNote(it)
|
||||
}.toSet().toList()
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -165,12 +164,11 @@ fun WatchPossibleNotificationChanges(
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
|
||||
val notifState by NotificationCache.live.observeAsState()
|
||||
val notif = remember(notifState) { notifState?.cache } ?: return
|
||||
val notifState by accountViewModel.accountLastReadLiveData.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = notifState, key2 = accountState) {
|
||||
launch(Dispatchers.IO) {
|
||||
onChange(route.hasNewItems(account, notif, emptySet()))
|
||||
onChange(route.hasNewItems(account, emptySet()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +176,7 @@ fun WatchPossibleNotificationChanges(
|
||||
launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect {
|
||||
launch(Dispatchers.IO) {
|
||||
onChange(route.hasNewItems(account, notif, it))
|
||||
onChange(route.hasNewItems(account, it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -229,38 +230,57 @@ fun ProfileContent(
|
||||
|
||||
@Composable
|
||||
private fun FollowingAndFollowerCounts(baseAccountUser: User) {
|
||||
val accountUserFollowsState by baseAccountUser.live().follows.observeAsState()
|
||||
|
||||
var followingCount by remember { mutableStateOf("--") }
|
||||
var followerCount by remember { mutableStateOf("--") }
|
||||
|
||||
LaunchedEffect(key1 = accountUserFollowsState) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newFollowing = accountUserFollowsState?.user?.cachedFollowCount()?.toString() ?: "--"
|
||||
val newFollower = accountUserFollowsState?.user?.cachedFollowerCount()?.toString() ?: "--"
|
||||
|
||||
if (followingCount != newFollowing) {
|
||||
followingCount = newFollowing
|
||||
}
|
||||
if (followerCount != newFollower) {
|
||||
followerCount = newFollower
|
||||
}
|
||||
WatchFollow(baseAccountUser = baseAccountUser) { newFollowing ->
|
||||
if (followingCount != newFollowing) {
|
||||
followingCount = newFollowing
|
||||
}
|
||||
}
|
||||
|
||||
Row() {
|
||||
Text(
|
||||
text = followingCount,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(stringResource(R.string.following))
|
||||
WatchFollower(baseAccountUser = baseAccountUser) { newFollower ->
|
||||
if (followerCount != newFollower) {
|
||||
followerCount = newFollower
|
||||
}
|
||||
}
|
||||
Row(modifier = Modifier.padding(start = 10.dp)) {
|
||||
Text(
|
||||
text = followerCount,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(stringResource(R.string.followers))
|
||||
|
||||
Text(
|
||||
text = followingCount,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(stringResource(R.string.following))
|
||||
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
|
||||
Text(
|
||||
text = followerCount,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(stringResource(R.string.followers))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchFollow(baseAccountUser: User, onReady: (String) -> Unit) {
|
||||
val accountUserFollowsState by baseAccountUser.live().follows.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = accountUserFollowsState) {
|
||||
launch(Dispatchers.IO) {
|
||||
onReady(accountUserFollowsState?.user?.cachedFollowCount()?.toString() ?: "--")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchFollower(baseAccountUser: User, onReady: (String) -> Unit) {
|
||||
val accountUserFollowersState by baseAccountUser.live().followers.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = accountUserFollowersState) {
|
||||
launch(Dispatchers.IO) {
|
||||
onReady(accountUserFollowersState?.user?.cachedFollowerCount()?.toString() ?: "--")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.navArgument
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
@@ -27,7 +26,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
sealed class Route(
|
||||
val route: String,
|
||||
val icon: Int,
|
||||
val hasNewItems: (Account, NotificationCache, Set<com.vitorpamplona.amethyst.model.Note>) -> Boolean = { _, _, _ -> false },
|
||||
val hasNewItems: (Account, Set<com.vitorpamplona.amethyst.model.Note>) -> Boolean = { _, _ -> false },
|
||||
val arguments: ImmutableList<NamedNavArgument> = persistentListOf()
|
||||
) {
|
||||
val base: String
|
||||
@@ -40,7 +39,7 @@ sealed class Route(
|
||||
navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false },
|
||||
navArgument("nip47") { type = NavType.StringType; nullable = true; defaultValue = null }
|
||||
).toImmutableList(),
|
||||
hasNewItems = { accountViewModel, cache, newNotes -> HomeLatestItem.hasNewItems(accountViewModel, cache, newNotes) }
|
||||
hasNewItems = { accountViewModel, newNotes -> HomeLatestItem.hasNewItems(accountViewModel, newNotes) }
|
||||
)
|
||||
|
||||
object Search : Route(
|
||||
@@ -59,13 +58,13 @@ sealed class Route(
|
||||
route = "Notification?scrollToTop={scrollToTop}",
|
||||
icon = R.drawable.ic_notifications,
|
||||
arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false }).toImmutableList(),
|
||||
hasNewItems = { accountViewModel, cache, newNotes -> NotificationLatestItem.hasNewItems(accountViewModel, cache, newNotes) }
|
||||
hasNewItems = { accountViewModel, newNotes -> NotificationLatestItem.hasNewItems(accountViewModel, newNotes) }
|
||||
)
|
||||
|
||||
object Message : Route(
|
||||
route = "Message",
|
||||
icon = R.drawable.ic_dm,
|
||||
hasNewItems = { accountViewModel, cache, newNotes -> MessagesLatestItem.hasNewItems(accountViewModel, cache, newNotes) }
|
||||
hasNewItems = { accountViewModel, newNotes -> MessagesLatestItem.hasNewItems(accountViewModel, newNotes) }
|
||||
)
|
||||
|
||||
object BlockedUsers : Route(
|
||||
@@ -127,29 +126,40 @@ fun currentRoute(navController: NavHostController): String? {
|
||||
open class LatestItem {
|
||||
var newestItemPerAccount: Map<String, Note?> = mapOf()
|
||||
|
||||
fun getNewestItem(account: Account): Note? {
|
||||
return newestItemPerAccount[account.userProfile().pubkeyHex]
|
||||
}
|
||||
|
||||
fun clearNewestItem(account: Account) {
|
||||
val userHex = account.userProfile().pubkeyHex
|
||||
if (newestItemPerAccount.contains(userHex)) {
|
||||
newestItemPerAccount = newestItemPerAccount - userHex
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNewestItem(newNotes: Set<Note>, account: Account, filter: AdditiveFeedFilter<Note>): Note? {
|
||||
val newestItem = newestItemPerAccount[account.userProfile().pubkeyHex]
|
||||
|
||||
if (newestItem == null) {
|
||||
newestItemPerAccount = newestItemPerAccount + Pair(
|
||||
account.userProfile().pubkeyHex,
|
||||
filterMore(filter.feed()).firstOrNull { it.createdAt() != null }
|
||||
filterMore(filter.feed(), account).firstOrNull { it.createdAt() != null }
|
||||
)
|
||||
} else {
|
||||
newestItemPerAccount = newestItemPerAccount + Pair(
|
||||
account.userProfile().pubkeyHex,
|
||||
filter.sort(filterMore(filter.applyFilter(newNotes)) + newestItem).first()
|
||||
filter.sort(filterMore(filter.applyFilter(newNotes), account) + newestItem).first()
|
||||
)
|
||||
}
|
||||
|
||||
return newestItemPerAccount[account.userProfile().pubkeyHex]
|
||||
}
|
||||
|
||||
open fun filterMore(newItems: Set<Note>): Set<Note> {
|
||||
open fun filterMore(newItems: Set<Note>, account: Account): Set<Note> {
|
||||
return newItems
|
||||
}
|
||||
|
||||
open fun filterMore(newItems: List<Note>): List<Note> {
|
||||
open fun filterMore(newItems: List<Note>, account: Account): List<Note> {
|
||||
return newItems
|
||||
}
|
||||
}
|
||||
@@ -157,10 +167,9 @@ open class LatestItem {
|
||||
object HomeLatestItem : LatestItem() {
|
||||
fun hasNewItems(
|
||||
account: Account,
|
||||
cache: NotificationCache,
|
||||
newNotes: Set<Note>
|
||||
): Boolean {
|
||||
val lastTime = cache.load("HomeFollows")
|
||||
val lastTime = account.loadLastRead("HomeFollows")
|
||||
|
||||
val newestItem = updateNewestItem(newNotes, account, HomeNewThreadFeedFilter(account))
|
||||
|
||||
@@ -171,10 +180,9 @@ object HomeLatestItem : LatestItem() {
|
||||
object NotificationLatestItem : LatestItem() {
|
||||
fun hasNewItems(
|
||||
account: Account,
|
||||
cache: NotificationCache,
|
||||
newNotes: Set<Note>
|
||||
): Boolean {
|
||||
val lastTime = cache.load("Notification")
|
||||
val lastTime = account.loadLastRead("Notification")
|
||||
|
||||
val newestItem = updateNewestItem(newNotes, account, NotificationFeedFilter(account))
|
||||
|
||||
@@ -185,24 +193,45 @@ object NotificationLatestItem : LatestItem() {
|
||||
object MessagesLatestItem : LatestItem() {
|
||||
fun hasNewItems(
|
||||
account: Account,
|
||||
cache: NotificationCache,
|
||||
newNotes: Set<Note>
|
||||
): Boolean {
|
||||
// Checks if the current newest item is still unread.
|
||||
// If so, there is no need to check anything else
|
||||
if (isNew(getNewestItem(account), account)) {
|
||||
return true
|
||||
}
|
||||
|
||||
clearNewestItem(account)
|
||||
|
||||
// gets the newest of the unread items
|
||||
val newestItem = updateNewestItem(newNotes, account, ChatroomListKnownFeedFilter(account))
|
||||
|
||||
val roomUserHex = (newestItem?.event as? PrivateDmEvent)?.talkingWith(account.userProfile().pubkeyHex)
|
||||
|
||||
val lastTime = cache.load("Room/$roomUserHex")
|
||||
|
||||
return (newestItem?.createdAt() ?: 0) > lastTime
|
||||
return isNew(newestItem, account)
|
||||
}
|
||||
|
||||
override fun filterMore(newItems: Set<Note>): Set<Note> {
|
||||
return newItems.filter { it.event is PrivateDmEvent }.toSet()
|
||||
fun isNew(it: Note?, account: Account): Boolean {
|
||||
if (it == null) return false
|
||||
|
||||
val currentUser = account.userProfile().pubkeyHex
|
||||
val room = (it.event as? PrivateDmEvent)?.talkingWith(currentUser)
|
||||
return if (room != null) {
|
||||
val lastRead = account.loadLastRead("Room/$room")
|
||||
(it.createdAt() ?: 0) > lastRead
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun filterMore(newItems: List<Note>): List<Note> {
|
||||
return newItems.filter { it.event is PrivateDmEvent }
|
||||
override fun filterMore(newItems: Set<Note>, account: Account): Set<Note> {
|
||||
return newItems.filter {
|
||||
isNew(it, account)
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
override fun filterMore(newItems: List<Note>, account: Account): List<Note> {
|
||||
return newItems.filter {
|
||||
isNew(it, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.screen.BadgeCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -62,9 +61,9 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
|
||||
|
||||
LaunchedEffect(key1 = likeSetCard) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val isNew = likeSetCard.createdAt() > NotificationCache.load(routeForLastRead)
|
||||
val isNew = likeSetCard.createdAt() > accountViewModel.account.loadLastRead(routeForLastRead)
|
||||
|
||||
NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt())
|
||||
accountViewModel.account.markAsRead(routeForLastRead, likeSetCard.createdAt())
|
||||
|
||||
val newBackgroundColor = if (isNew) {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
|
||||
@Composable
|
||||
@@ -88,7 +89,7 @@ fun HiddenNote(
|
||||
baseNote = it,
|
||||
nav = nav,
|
||||
accountViewModel = accountViewModel,
|
||||
size = 35.dp
|
||||
size = Size35dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
@@ -74,7 +73,7 @@ fun ChatroomCompose(
|
||||
BlankNote(Modifier)
|
||||
} else if (channelHex != null) {
|
||||
LoadChannel(baseChannelHex = channelHex!!) { channel ->
|
||||
ChannelRoomCompose(note, channel, nav)
|
||||
ChannelRoomCompose(note, channel, accountViewModel, nav)
|
||||
}
|
||||
} else {
|
||||
val userRoomHex = remember(noteState, accountViewModel) {
|
||||
@@ -91,6 +90,7 @@ fun ChatroomCompose(
|
||||
private fun ChannelRoomCompose(
|
||||
note: Note,
|
||||
channel: Channel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val authorState by note.author!!.live().metadata.observeAsState()
|
||||
@@ -128,7 +128,7 @@ private fun ChannelRoomCompose(
|
||||
|
||||
var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
|
||||
|
||||
WatchNotificationChanges(note, route) { newHasNewMessages ->
|
||||
WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages ->
|
||||
if (hasNewMessages != newHasNewMessages) {
|
||||
hasNewMessages = newHasNewMessages
|
||||
}
|
||||
@@ -182,7 +182,7 @@ private fun UserRoomCompose(
|
||||
"Room/${user.pubkeyHex}"
|
||||
}
|
||||
|
||||
WatchNotificationChanges(note, route) { newHasNewMessages ->
|
||||
WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages ->
|
||||
if (hasNewMessages != newHasNewMessages) {
|
||||
hasNewMessages = newHasNewMessages
|
||||
}
|
||||
@@ -208,14 +208,15 @@ private fun UserRoomCompose(
|
||||
private fun WatchNotificationChanges(
|
||||
note: Note,
|
||||
route: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
onNewStatus: (Boolean) -> Unit
|
||||
) {
|
||||
val cacheState by NotificationCache.live.observeAsState()
|
||||
val cacheState by accountViewModel.accountLastReadLiveData.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = note, cacheState) {
|
||||
launch(Dispatchers.IO) {
|
||||
note.event?.createdAt()?.let {
|
||||
val lastTime = NotificationCache.load(route)
|
||||
val lastTime = accountViewModel.account.loadLastRead(route)
|
||||
onNewStatus(it > lastTime)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||
@@ -71,6 +70,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeMe
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeThem
|
||||
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
@@ -183,11 +183,11 @@ fun ChatroomMessageCompose(
|
||||
LaunchedEffect(key1 = routeForLastRead) {
|
||||
routeForLastRead?.let {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val lastTime = NotificationCache.load(it)
|
||||
val lastTime = accountViewModel.account.loadLastRead(it)
|
||||
|
||||
val createdAt = note.createdAt()
|
||||
if (createdAt != null) {
|
||||
NotificationCache.markAsRead(it, createdAt)
|
||||
accountViewModel.account.markAsRead(it, createdAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -498,15 +498,22 @@ private fun DrawAuthorInfo(
|
||||
}
|
||||
)
|
||||
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = remember { " $userDisplayName" },
|
||||
suffix = "",
|
||||
tags = userTags,
|
||||
fontWeight = FontWeight.Bold,
|
||||
overrideColor = MaterialTheme.colors.onBackground,
|
||||
route = route,
|
||||
nav = nav
|
||||
)
|
||||
userDisplayName?.let {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = it,
|
||||
suffix = "",
|
||||
tags = userTags,
|
||||
fontWeight = FontWeight.Bold,
|
||||
overrideColor = MaterialTheme.colors.onBackground,
|
||||
route = route,
|
||||
nav = nav
|
||||
)
|
||||
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
DrawPlayName(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||
import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
@@ -54,9 +52,9 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
||||
|
||||
LaunchedEffect(key1 = messageSetCard) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val isNew = messageSetCard.createdAt() > NotificationCache.load(routeForLastRead)
|
||||
val isNew = messageSetCard.createdAt() > accountViewModel.account.loadLastRead(routeForLastRead)
|
||||
|
||||
NotificationCache.markAsRead(routeForLastRead, messageSetCard.createdAt())
|
||||
accountViewModel.account.markAsRead(routeForLastRead, messageSetCard.createdAt())
|
||||
|
||||
val newBackgroundColor = if (isNew) {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
@@ -101,16 +99,12 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
||||
MessageIcon()
|
||||
|
||||
Column(modifier = remember { Modifier.padding(start = 10.dp) }) {
|
||||
val routeForLastRead = remember(baseNote) {
|
||||
"Room/${(baseNote.event as? PrivateDmEvent)?.talkingWith(loggedIn.pubkeyHex)}"
|
||||
}
|
||||
|
||||
NoteCompose(
|
||||
baseNote = baseNote,
|
||||
routeForLastRead = routeForLastRead,
|
||||
routeForLastRead = null,
|
||||
isBoostedNote = true,
|
||||
addMarginTop = false,
|
||||
parentBackgroundColor = null,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
|
||||
@@ -43,7 +43,6 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
@@ -58,6 +57,7 @@ import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.overPictureBackground
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -78,9 +78,9 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
||||
|
||||
LaunchedEffect(key1 = multiSetCard) {
|
||||
launch(Dispatchers.IO) {
|
||||
val isNew = multiSetCard.maxCreatedAt > NotificationCache.load(routeForLastRead)
|
||||
val isNew = multiSetCard.maxCreatedAt > accountViewModel.account.loadLastRead(routeForLastRead)
|
||||
|
||||
NotificationCache.markAsRead(routeForLastRead, multiSetCard.maxCreatedAt)
|
||||
accountViewModel.account.markAsRead(routeForLastRead, multiSetCard.maxCreatedAt)
|
||||
|
||||
val newBackgroundColor = if (isNew) {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
@@ -367,9 +367,7 @@ val textBoxModifier = Modifier.padding(start = 5.dp).fillMaxWidth()
|
||||
|
||||
val simpleModifier = Modifier
|
||||
|
||||
val size = 35.dp
|
||||
|
||||
val sizedModifier = Modifier.size(size)
|
||||
val sizedModifier = Modifier.size(Size35dp)
|
||||
|
||||
val bottomPadding1dp = Modifier.padding(bottom = 1.dp)
|
||||
|
||||
@@ -398,7 +396,7 @@ private fun AuthorPictureAndComment(
|
||||
Box(modifier = sizedModifier, contentAlignment = Alignment.BottomCenter) {
|
||||
FastNoteAuthorPicture(
|
||||
author = author,
|
||||
size = size,
|
||||
size = Size35dp,
|
||||
accountViewModel = accountViewModel,
|
||||
pictureModifier = simpleModifier
|
||||
)
|
||||
|
||||
@@ -82,7 +82,6 @@ import androidx.core.graphics.get
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.AsyncImagePainter
|
||||
import coil.request.SuccessResult
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
@@ -99,7 +98,9 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||
import com.vitorpamplona.amethyst.service.model.EventInterface
|
||||
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
|
||||
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
|
||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.Participant
|
||||
import com.vitorpamplona.amethyst.service.model.PeopleListEvent
|
||||
@@ -142,6 +143,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.Following
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
@@ -423,11 +425,11 @@ private fun CheckNewAndRenderNote(
|
||||
LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) {
|
||||
launch(Dispatchers.IO) {
|
||||
routeForLastRead?.let {
|
||||
val lastTime = NotificationCache.load(it)
|
||||
val lastTime = accountViewModel.account.loadLastRead(it)
|
||||
|
||||
val createdAt = baseNote.createdAt()
|
||||
if (createdAt != null) {
|
||||
NotificationCache.markAsRead(it, createdAt)
|
||||
accountViewModel.account.markAsRead(it, createdAt)
|
||||
|
||||
val isNew = createdAt > lastTime
|
||||
|
||||
@@ -519,7 +521,7 @@ private fun RenderNoteWithReactions(
|
||||
|
||||
val showSecondRow by remember {
|
||||
derivedStateOf {
|
||||
baseNote.event !is RepostEvent && !isBoostedNote && !isQuotedNote
|
||||
baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent && !isBoostedNote && !isQuotedNote
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,7 +555,7 @@ private fun RenderNoteWithReactions(
|
||||
)
|
||||
}
|
||||
|
||||
if (!makeItShort && baseNote.event !is RepostEvent) {
|
||||
if (!makeItShort && baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent) {
|
||||
ReactionsRow(
|
||||
baseNote = baseNote,
|
||||
showReactionDetail = notBoostedNorQuote,
|
||||
@@ -561,7 +563,7 @@ private fun RenderNoteWithReactions(
|
||||
nav = nav
|
||||
)
|
||||
} else {
|
||||
if (!isBoostedNote && baseNote.event !is RepostEvent) {
|
||||
if (!isBoostedNote && baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent) {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
}
|
||||
@@ -647,6 +649,10 @@ private fun RenderNoteRow(
|
||||
RenderRepost(baseNote, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is GenericRepostEvent -> {
|
||||
RenderRepost(baseNote, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is ReportEvent -> {
|
||||
RenderReport(baseNote, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
@@ -675,6 +681,10 @@ private fun RenderNoteRow(
|
||||
RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is LiveActivitiesEvent -> {
|
||||
RenderLiveActivityEvent(baseNote, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is PrivateDmEvent -> {
|
||||
RenderPrivateMessage(
|
||||
baseNote,
|
||||
@@ -947,7 +957,7 @@ fun RenderAppDefinition(
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(35.dp)
|
||||
.height(Size35dp)
|
||||
.padding(bottom = 3.dp)
|
||||
) {
|
||||
}
|
||||
@@ -1354,7 +1364,7 @@ private fun RenderBadgeAward(
|
||||
awardees.take(100).forEach { user ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.size(size = 35.dp)
|
||||
.size(size = Size35dp)
|
||||
.clickable {
|
||||
nav("User/${user.pubkeyHex}")
|
||||
},
|
||||
@@ -1363,7 +1373,7 @@ private fun RenderBadgeAward(
|
||||
UserPicture(
|
||||
baseUser = user,
|
||||
accountViewModel = accountViewModel,
|
||||
size = 35.dp
|
||||
size = Size35dp
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1416,7 +1426,7 @@ private fun RenderReaction(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderRepost(
|
||||
fun RenderRepost(
|
||||
note: Note,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -1749,7 +1759,7 @@ private fun FirstUserInfoRow(
|
||||
NoteUsernameDisplay(baseNote, remember { Modifier.weight(1f) })
|
||||
}
|
||||
|
||||
if (eventNote is RepostEvent) {
|
||||
if (eventNote is RepostEvent || eventNote is GenericRepostEvent) {
|
||||
Text(
|
||||
" ${stringResource(id = R.string.boosted)}",
|
||||
fontWeight = FontWeight.Bold,
|
||||
@@ -1823,7 +1833,7 @@ private fun DrawAuthorImages(baseNote: Note, accountViewModel: AccountViewModel,
|
||||
Box(modifier = modifier, contentAlignment = Alignment.BottomEnd) {
|
||||
NoteAuthorPicture(baseNote, nav, accountViewModel, 55.dp)
|
||||
|
||||
if (baseNote.event is RepostEvent) {
|
||||
if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) {
|
||||
RepostNoteAuthorPicture(baseNote, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@@ -1834,7 +1844,7 @@ private fun DrawAuthorImages(baseNote: Note, accountViewModel: AccountViewModel,
|
||||
}
|
||||
}
|
||||
|
||||
if (baseNote.event is RepostEvent) {
|
||||
if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) {
|
||||
val baseReply = remember {
|
||||
baseNote.replyTo?.lastOrNull()
|
||||
}
|
||||
@@ -2462,6 +2472,80 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, accountViewModel: AccountViewMo
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderLiveActivityEvent(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val noteEvent = baseNote.event as? LiveActivitiesEvent ?: return
|
||||
|
||||
val media = remember { noteEvent.streaming() }
|
||||
val cover = remember { noteEvent.image() }
|
||||
val subject = remember { noteEvent.title() }
|
||||
val content = remember { noteEvent.summary() }
|
||||
val participants = remember { noteEvent.participants() }
|
||||
|
||||
var participantUsers by remember { mutableStateOf<List<Pair<Participant, User>>>(emptyList()) }
|
||||
|
||||
LaunchedEffect(key1 = participants) {
|
||||
launch(Dispatchers.IO) {
|
||||
participantUsers = participants.mapNotNull { part ->
|
||||
LocalCache.checkGetOrCreateUser(part.key)?.let { Pair(part, it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.padding(top = 5.dp)) {
|
||||
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Row() {
|
||||
subject?.let {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)) {
|
||||
Text(
|
||||
text = it,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
participantUsers.forEach {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(top = 5.dp, start = 10.dp, end = 10.dp)
|
||||
.clickable {
|
||||
nav("User/${it.second.pubkeyHex}")
|
||||
}
|
||||
) {
|
||||
UserPicture(it.second, 25.dp, accountViewModel)
|
||||
Spacer(Modifier.width(5.dp))
|
||||
UsernameDisplay(it.second, Modifier.weight(1f))
|
||||
Spacer(Modifier.width(5.dp))
|
||||
it.first.role?.let {
|
||||
Text(
|
||||
text = it.capitalize(Locale.ROOT),
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
media?.let { media ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(10.dp)
|
||||
) {
|
||||
VideoView(
|
||||
videoUri = media,
|
||||
description = subject
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, accountViewModel: AccountViewModel) {
|
||||
val image = remember(noteEvent) { noteEvent.image() }
|
||||
@@ -2622,7 +2706,9 @@ private fun VerticalRelayPanelWithFlow(
|
||||
|
||||
val showMoreRelaysButtonIconButtonModifier = Modifier.size(24.dp)
|
||||
val showMoreRelaysButtonIconModifier = Modifier.size(15.dp)
|
||||
val showMoreRelaysButtonBoxModifer = Modifier.fillMaxWidth().height(25.dp)
|
||||
val showMoreRelaysButtonBoxModifer = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(25.dp)
|
||||
|
||||
@Composable
|
||||
private fun ShowMoreRelaysButton(onClick: () -> Unit) {
|
||||
|
||||
@@ -7,11 +7,13 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -21,6 +23,7 @@ import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ProgressIndicatorDefaults
|
||||
import androidx.compose.material.Text
|
||||
@@ -54,6 +57,7 @@ import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Popup
|
||||
@@ -68,6 +72,7 @@ import com.vitorpamplona.amethyst.ui.screen.CombinedZap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
@@ -86,6 +91,10 @@ fun ReactionsRow(baseNote: Note, showReactionDetail: Boolean, accountViewModel:
|
||||
mutableStateOf<Boolean>(false)
|
||||
}
|
||||
|
||||
val zapraiserAmount = remember {
|
||||
baseNote.event?.zapraiserAmount()
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(7.dp))
|
||||
|
||||
Row(verticalAlignment = CenterVertically, modifier = Modifier.padding(start = 10.dp)) {
|
||||
@@ -118,6 +127,19 @@ fun ReactionsRow(baseNote: Note, showReactionDetail: Boolean, accountViewModel:
|
||||
}
|
||||
}
|
||||
|
||||
if (zapraiserAmount != null && zapraiserAmount > 0) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = if (showReactionDetail) 75.dp else 0.dp),
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
RenderZapRaiser(baseNote, zapraiserAmount, wantsToSeeReactions.value, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
if (showReactionDetail && wantsToSeeReactions.value) {
|
||||
ReactionDetailGallery(baseNote, nav, accountViewModel)
|
||||
}
|
||||
@@ -125,6 +147,70 @@ fun ReactionsRow(baseNote: Note, showReactionDetail: Boolean, accountViewModel:
|
||||
Spacer(modifier = Modifier.height(7.dp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, accountViewModel: AccountViewModel) {
|
||||
val zapsState by baseNote.live().zaps.observeAsState()
|
||||
|
||||
var zapraiserProgress by remember { mutableStateOf(0F) }
|
||||
var zapraiserLeft by remember { mutableStateOf("$zapraiserAmount") }
|
||||
|
||||
LaunchedEffect(key1 = zapsState) {
|
||||
launch(Dispatchers.Default) {
|
||||
zapsState?.note?.let {
|
||||
val newZapAmount = accountViewModel.calculateZapAmount(it)
|
||||
var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat()
|
||||
|
||||
if (percentage > 1) {
|
||||
percentage = 1f
|
||||
}
|
||||
|
||||
if (Math.abs(zapraiserProgress - percentage) > 0.001) {
|
||||
zapraiserProgress = percentage
|
||||
if (percentage > 0.99) {
|
||||
zapraiserLeft = "0"
|
||||
} else {
|
||||
zapraiserLeft = showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val color = if (zapraiserProgress > 0.99) {
|
||||
Color.Green.copy(alpha = 0.32f)
|
||||
} else {
|
||||
MaterialTheme.colors.mediumImportanceLink
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier.padding(end = 10.dp).fillMaxWidth()
|
||||
) {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.matchParentSize(),
|
||||
color = color,
|
||||
progress = zapraiserProgress
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.padding(2.dp)
|
||||
) {
|
||||
if (details) {
|
||||
val totalPercentage = remember(zapraiserProgress) {
|
||||
"${(zapraiserProgress * 100).roundToInt()}%"
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.sats_to_complete, totalPercentage, zapraiserLeft),
|
||||
modifier = Modifier.padding(start = 5.dp, end = 5.dp, top = 2.dp, bottom = 2.dp),
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExpandButton(baseNote: Note, wantsToSeeReactions: MutableState<Boolean>) {
|
||||
val zapsState by baseNote.live().zaps.observeAsState()
|
||||
@@ -441,7 +527,8 @@ fun LikeReaction(
|
||||
grayTint: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
iconSize: Dp = 20.dp,
|
||||
heartSize: Dp = 16.dp
|
||||
heartSize: Dp = 16.dp,
|
||||
iconFontSize: TextUnit = 14.sp
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -455,6 +542,7 @@ fun LikeReaction(
|
||||
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = iconButtonModifier.combinedClickable(
|
||||
role = Role.Button,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
@@ -475,7 +563,7 @@ fun LikeReaction(
|
||||
}
|
||||
)
|
||||
) {
|
||||
LikeIcon(baseNote, heartSize, grayTint, accountViewModel.userProfile())
|
||||
LikeIcon(baseNote, iconFontSize, heartSize, grayTint, accountViewModel.userProfile())
|
||||
|
||||
if (wantsToChangeReactionSymbol) {
|
||||
UpdateReactionTypeDialog({ wantsToChangeReactionSymbol = false }, accountViewModel = accountViewModel)
|
||||
@@ -500,7 +588,13 @@ fun LikeReaction(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LikeIcon(baseNote: Note, iconSize: Dp = 20.dp, grayTint: Color, loggedIn: User) {
|
||||
fun LikeIcon(
|
||||
baseNote: Note,
|
||||
iconFontSize: TextUnit = 14.sp,
|
||||
iconSize: Dp = 20.dp,
|
||||
grayTint: Color,
|
||||
loggedIn: User
|
||||
) {
|
||||
val reactionsState by baseNote.live().reactions.observeAsState()
|
||||
|
||||
var reactionType by remember(baseNote) {
|
||||
@@ -511,7 +605,7 @@ fun LikeIcon(baseNote: Note, iconSize: Dp = 20.dp, grayTint: Color, loggedIn: Us
|
||||
launch(Dispatchers.Default) {
|
||||
val newReactionType = reactionsState?.note?.isReactedBy(loggedIn)
|
||||
if (reactionType != newReactionType) {
|
||||
reactionType = newReactionType?.take(2) ?: "+"
|
||||
reactionType = newReactionType?.take(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -530,8 +624,8 @@ fun LikeIcon(baseNote: Note, iconSize: Dp = 20.dp, grayTint: Color, loggedIn: Us
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
"-" -> Text(text = "\uD83D\uDC4E")
|
||||
else -> Text(text = reactionType!!)
|
||||
"-" -> Text(text = "\uD83D\uDC4E", fontSize = iconFontSize)
|
||||
else -> Text(text = reactionType!!, fontSize = iconFontSize)
|
||||
}
|
||||
} else {
|
||||
Icon(
|
||||
|
||||
@@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||
@@ -178,9 +179,9 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
|
||||
reactions[netDate] = (reactions[netDate] ?: 0) + 1
|
||||
takenIntoAccount.add(noteEvent.id())
|
||||
}
|
||||
} else if (noteEvent is RepostEvent) {
|
||||
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) {
|
||||
val netDate = formatDate(noteEvent.createdAt)
|
||||
} else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) {
|
||||
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey() != currentUser) {
|
||||
val netDate = formatDate(noteEvent.createdAt())
|
||||
boosts[netDate] = (boosts[netDate] ?: 0) + 1
|
||||
takenIntoAccount.add(noteEvent.id())
|
||||
}
|
||||
@@ -229,9 +230,9 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
|
||||
takenIntoAccount.add(noteEvent.id())
|
||||
hasNewElements = true
|
||||
}
|
||||
} else if (noteEvent is RepostEvent) {
|
||||
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) {
|
||||
val netDate = formatDate(noteEvent.createdAt)
|
||||
} else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) {
|
||||
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey() != currentUser) {
|
||||
val netDate = formatDate(noteEvent.createdAt())
|
||||
boosts[netDate] = (boosts[netDate] ?: 0) + 1
|
||||
takenIntoAccount.add(noteEvent.id())
|
||||
hasNewElements = true
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.PlayCircle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.tts.TextToSpeechHelper
|
||||
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdButtonSizeModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@Composable
|
||||
@@ -59,6 +70,7 @@ private fun UserNameDisplay(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = modifier
|
||||
)
|
||||
DrawPlayName(bestDisplayName)
|
||||
} else if (bestDisplayName != null) {
|
||||
CreateTextWithEmoji(
|
||||
text = bestDisplayName,
|
||||
@@ -68,6 +80,7 @@ private fun UserNameDisplay(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = modifier
|
||||
)
|
||||
DrawPlayName(bestDisplayName)
|
||||
} else if (bestUserName != null) {
|
||||
CreateTextWithEmoji(
|
||||
text = remember { "@$bestUserName" },
|
||||
@@ -77,6 +90,7 @@ private fun UserNameDisplay(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = modifier
|
||||
)
|
||||
DrawPlayName(bestUserName)
|
||||
} else {
|
||||
Text(
|
||||
npubDisplay,
|
||||
@@ -87,3 +101,39 @@ private fun UserNameDisplay(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DrawPlayName(name: String) {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
IconButton(
|
||||
onClick = { speak(name, context, lifecycleOwner) },
|
||||
modifier = StdButtonSizeModifier
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.PlayCircle,
|
||||
contentDescription = null,
|
||||
modifier = StdButtonSizeModifier,
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun speak(
|
||||
message: String,
|
||||
context: Context,
|
||||
owner: LifecycleOwner
|
||||
) {
|
||||
TextToSpeechHelper
|
||||
.getInstance(context)
|
||||
.registerLifecycle(owner)
|
||||
.speak(message)
|
||||
.highlight()
|
||||
.onDone {
|
||||
Log.d("TextToSpeak", "speak: done")
|
||||
}
|
||||
.onError {
|
||||
Log.d("TextToSpeak", "speak error: $it")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.screen.ZapUserSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -43,9 +42,9 @@ fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false,
|
||||
|
||||
LaunchedEffect(key1 = zapSetCard.createdAt()) {
|
||||
launch(Dispatchers.IO) {
|
||||
val isNew = zapSetCard.createdAt > NotificationCache.load(routeForLastRead)
|
||||
val isNew = zapSetCard.createdAt > accountViewModel.account.loadLastRead(routeForLastRead)
|
||||
|
||||
NotificationCache.markAsRead(routeForLastRead, zapSetCard.createdAt)
|
||||
accountViewModel.account.markAsRead(routeForLastRead, zapSetCard.createdAt)
|
||||
|
||||
val newBackgroundColor = if (isNew) {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
|
||||
@@ -39,6 +39,7 @@ import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.qrcode.NIP19QrCodeScanner
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
|
||||
@Composable
|
||||
fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
|
||||
@@ -99,7 +100,7 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 35.dp)
|
||||
.padding(horizontal = Size35dp)
|
||||
) {
|
||||
QrCodeDrawer("nostr:${user.pubkeyNpub()}")
|
||||
}
|
||||
@@ -112,7 +113,7 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
|
||||
) {
|
||||
Button(
|
||||
onClick = { presenting = false },
|
||||
shape = RoundedCornerShape(35.dp),
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
||||
@@ -162,7 +163,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
|
||||
val boostsPerEvent = mutableMapOf<Note, MutableList<Note>>()
|
||||
notes
|
||||
.filter { it.event is RepostEvent }
|
||||
.filter { it.event is RepostEvent || it.event is GenericRepostEvent }
|
||||
.forEach {
|
||||
val boostedPost = it.replyTo?.lastOrNull() { it.event !is ChannelMetadataEvent && it.event !is ChannelCreateEvent }
|
||||
if (boostedPost != null) {
|
||||
@@ -197,7 +198,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
)
|
||||
}
|
||||
|
||||
val textNoteCards = notes.filter { it.event !is ReactionEvent && it.event !is RepostEvent && it.event !is LnZapEvent }.map {
|
||||
val textNoteCards = notes.filter { it.event !is ReactionEvent && it.event !is RepostEvent && it.event !is GenericRepostEvent && it.event !is LnZapEvent }.map {
|
||||
if (it.event is PrivateDmEvent) {
|
||||
MessageSetCard(it)
|
||||
} else if (it.event is BadgeAwardEvent) {
|
||||
|
||||
@@ -26,7 +26,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.ChatroomCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -103,7 +102,7 @@ private fun FeedLoaded(
|
||||
"Room/$roomUser"
|
||||
}
|
||||
|
||||
NotificationCache.markAsRead(route, it.createdAt())
|
||||
accountViewModel.account.markAsRead(route, it.createdAt())
|
||||
}
|
||||
}
|
||||
markAsRead.value = false
|
||||
|
||||
@@ -57,12 +57,14 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.model.AppDefinitionEvent
|
||||
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
||||
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PeopleListEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PinListEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.RelaySetEvent
|
||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.note.*
|
||||
import com.vitorpamplona.amethyst.ui.note.BadgeDisplay
|
||||
@@ -401,6 +403,8 @@ fun NoteMaster(
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
} else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) {
|
||||
RenderRepost(baseNote, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is PollNoteEvent) {
|
||||
val canPreview = note.author == account.userProfile() ||
|
||||
(note.author?.let { account.userProfile().isFollowingCached(it) } ?: true) ||
|
||||
|
||||
@@ -30,6 +30,7 @@ import java.util.Locale
|
||||
class AccountViewModel(val account: Account) : ViewModel() {
|
||||
val accountLiveData: LiveData<AccountState> = account.live.map { it }
|
||||
val accountLanguagesLiveData: LiveData<AccountState> = account.liveLanguages.map { it }
|
||||
val accountLastReadLiveData: LiveData<AccountState> = account.liveLastRead.map { it }
|
||||
|
||||
fun isWriteable(): Boolean {
|
||||
return account.isWriteable()
|
||||
|
||||
@@ -80,6 +80,7 @@ import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -396,11 +397,11 @@ fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, nav:
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RobohashAsyncImageProxy(
|
||||
robot = channel.idHex,
|
||||
model = ResizeImage(channel.profilePicture(), 35.dp),
|
||||
model = ResizeImage(channel.profilePicture(), Size35dp),
|
||||
contentDescription = context.getString(R.string.profile_image),
|
||||
modifier = Modifier
|
||||
.width(35.dp)
|
||||
.height(35.dp)
|
||||
.width(Size35dp)
|
||||
.height(Size35dp)
|
||||
.clip(shape = CircleShape)
|
||||
)
|
||||
|
||||
@@ -429,7 +430,7 @@ fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, nav:
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(35.dp)
|
||||
.height(Size35dp)
|
||||
.padding(bottom = 3.dp)
|
||||
) {
|
||||
ChannelActionOptions(channel, accountViewModel, nav)
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -198,7 +199,7 @@ fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, nav: (Str
|
||||
UserPicture(
|
||||
baseUser = baseUser,
|
||||
accountViewModel = accountViewModel,
|
||||
size = 35.dp
|
||||
size = Size35dp
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
|
||||
@@ -138,19 +138,21 @@ fun WatchAccountForHomeScreen(
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
|
||||
var firstTime by remember(accountViewModel) { mutableStateOf(true) }
|
||||
val followState by accountViewModel.account.userProfile().live().follows.observeAsState()
|
||||
|
||||
LaunchedEffect(accountViewModel, accountState?.account?.defaultHomeFollowList) {
|
||||
// Only invalidate when things change. Not in the first run
|
||||
if (firstTime) {
|
||||
firstTime = false
|
||||
} else {
|
||||
launch(Dispatchers.IO) {
|
||||
NostrHomeDataSource.invalidateFilters()
|
||||
homeFeedViewModel.invalidateDataAndSendToTop(true)
|
||||
repliesFeedViewModel.invalidateDataAndSendToTop(true)
|
||||
}
|
||||
launch(Dispatchers.IO) {
|
||||
NostrHomeDataSource.invalidateFilters()
|
||||
homeFeedViewModel.invalidateDataAndSendToTop(true)
|
||||
repliesFeedViewModel.invalidateDataAndSendToTop(true)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(followState) {
|
||||
launch(Dispatchers.IO) {
|
||||
NostrHomeDataSource.invalidateFilters()
|
||||
homeFeedViewModel.invalidateData(true)
|
||||
repliesFeedViewModel.invalidateData(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+185
-103
@@ -9,6 +9,7 @@ import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.gestures.scrollBy
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
@@ -97,6 +98,7 @@ import com.vitorpamplona.amethyst.ui.screen.RelayFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -108,13 +110,16 @@ import java.math.BigDecimal
|
||||
fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
if (userId == null) return
|
||||
|
||||
var userBase by remember { mutableStateOf<User?>(null) }
|
||||
var userBase by remember { mutableStateOf<User?>(LocalCache.getUserIfExists(userId)) }
|
||||
|
||||
LaunchedEffect(userId) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val newUserBase = LocalCache.checkGetOrCreateUser(userId)
|
||||
if (newUserBase != userBase) {
|
||||
userBase = newUserBase
|
||||
if (userBase == null) {
|
||||
// waits to resolve.
|
||||
withContext(Dispatchers.IO) {
|
||||
val newUserBase = LocalCache.checkGetOrCreateUser(userId)
|
||||
if (newUserBase != userBase) {
|
||||
userBase = newUserBase
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,7 +262,7 @@ fun ProfileScreen(
|
||||
})
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
Column(modifier = Modifier.padding()) {
|
||||
Column() {
|
||||
ProfileHeader(baseUser, appRecommendations, nav, accountViewModel)
|
||||
ScrollableTabRow(
|
||||
backgroundColor = MaterialTheme.colors.background,
|
||||
@@ -267,26 +272,7 @@ fun ProfileScreen(
|
||||
tabsSize = it
|
||||
}
|
||||
) {
|
||||
val tabs = listOf<@Composable() (() -> Unit)?>(
|
||||
{ Text(text = stringResource(R.string.notes)) },
|
||||
{ Text(text = stringResource(R.string.replies)) },
|
||||
{ FollowTabHeader(baseUser) },
|
||||
{ FollowersTabHeader(baseUser) },
|
||||
{ ZapTabHeader(baseUser) },
|
||||
{ BookmarkTabHeader(baseUser) },
|
||||
{ ReportsTabHeader(baseUser) },
|
||||
{ RelaysTabHeader(baseUser) }
|
||||
)
|
||||
|
||||
tabs.forEachIndexed { index, function ->
|
||||
Tab(
|
||||
selected = pagerState.currentPage == index,
|
||||
onClick = {
|
||||
coroutineScope.launch { pagerState.animateScrollToPage(index) }
|
||||
},
|
||||
text = function
|
||||
)
|
||||
}
|
||||
CreateAndRenderTabs(baseUser, pagerState)
|
||||
}
|
||||
HorizontalPager(
|
||||
pageCount = 8,
|
||||
@@ -295,16 +281,15 @@ fun ProfileScreen(
|
||||
Modifier.height((columnSize.height - tabsSize.height).toDp())
|
||||
}
|
||||
) { page ->
|
||||
when (page) {
|
||||
0 -> TabNotesNewThreads(accountViewModel, nav)
|
||||
1 -> TabNotesConversations(accountViewModel, nav)
|
||||
2 -> TabFollows(baseUser, followsFeedViewModel, accountViewModel, nav)
|
||||
3 -> TabFollows(baseUser, followersFeedViewModel, accountViewModel, nav)
|
||||
4 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav)
|
||||
5 -> TabBookmarks(baseUser, accountViewModel, nav)
|
||||
6 -> TabReports(baseUser, accountViewModel, nav)
|
||||
7 -> TabRelays(baseUser, accountViewModel)
|
||||
}
|
||||
CreateAndRenderPages(
|
||||
page,
|
||||
baseUser,
|
||||
followsFeedViewModel,
|
||||
followersFeedViewModel,
|
||||
zapFeedViewModel,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -312,6 +297,58 @@ fun ProfileScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CreateAndRenderPages(
|
||||
page: Int,
|
||||
baseUser: User,
|
||||
followsFeedViewModel: NostrUserProfileFollowsUserFeedViewModel,
|
||||
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
|
||||
zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
when (page) {
|
||||
0 -> TabNotesNewThreads(accountViewModel, nav)
|
||||
1 -> TabNotesConversations(accountViewModel, nav)
|
||||
2 -> TabFollows(baseUser, followsFeedViewModel, accountViewModel, nav)
|
||||
3 -> TabFollowers(baseUser, followersFeedViewModel, accountViewModel, nav)
|
||||
4 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav)
|
||||
5 -> TabBookmarks(baseUser, accountViewModel, nav)
|
||||
6 -> TabReports(baseUser, accountViewModel, nav)
|
||||
7 -> TabRelays(baseUser, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun CreateAndRenderTabs(
|
||||
baseUser: User,
|
||||
pagerState: PagerState
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val tabs = listOf<@Composable() (() -> Unit)?>(
|
||||
{ Text(text = stringResource(R.string.notes)) },
|
||||
{ Text(text = stringResource(R.string.replies)) },
|
||||
{ FollowTabHeader(baseUser) },
|
||||
{ FollowersTabHeader(baseUser) },
|
||||
{ ZapTabHeader(baseUser) },
|
||||
{ BookmarkTabHeader(baseUser) },
|
||||
{ ReportsTabHeader(baseUser) },
|
||||
{ RelaysTabHeader(baseUser) }
|
||||
)
|
||||
|
||||
tabs.forEachIndexed { index, function ->
|
||||
Tab(
|
||||
selected = pagerState.currentPage == index,
|
||||
onClick = {
|
||||
coroutineScope.launch { pagerState.animateScrollToPage(index) }
|
||||
},
|
||||
text = function
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RelaysTabHeader(baseUser: User) {
|
||||
val userState by baseUser.live().relays.observeAsState()
|
||||
@@ -382,12 +419,14 @@ private fun ZapTabHeader(baseUser: User) {
|
||||
|
||||
@Composable
|
||||
private fun FollowersTabHeader(baseUser: User) {
|
||||
val userState by baseUser.live().follows.observeAsState()
|
||||
val userState by baseUser.live().followers.observeAsState()
|
||||
var followerCount by remember { mutableStateOf("--") }
|
||||
|
||||
val text = stringResource(R.string.followers)
|
||||
|
||||
LaunchedEffect(key1 = userState) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newFollower = userState?.user?.transientFollowerCount()?.toString() ?: "--"
|
||||
val newFollower = (userState?.user?.transientFollowerCount()?.toString() ?: "--") + " " + text
|
||||
|
||||
if (followerCount != newFollower) {
|
||||
followerCount = newFollower
|
||||
@@ -395,7 +434,7 @@ private fun FollowersTabHeader(baseUser: User) {
|
||||
}
|
||||
}
|
||||
|
||||
Text(text = "$followerCount ${stringResource(id = R.string.followers)}")
|
||||
Text(text = followerCount)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -403,9 +442,11 @@ private fun FollowTabHeader(baseUser: User) {
|
||||
val userState by baseUser.live().follows.observeAsState()
|
||||
var followCount by remember { mutableStateOf("--") }
|
||||
|
||||
val text = stringResource(R.string.follows)
|
||||
|
||||
LaunchedEffect(key1 = userState) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newFollow = userState?.user?.transientFollowCount()?.toString() ?: "--"
|
||||
val newFollow = (userState?.user?.transientFollowCount()?.toString() ?: "--") + " " + text
|
||||
|
||||
if (followCount != newFollow) {
|
||||
followCount = newFollow
|
||||
@@ -413,7 +454,7 @@ private fun FollowTabHeader(baseUser: User) {
|
||||
}
|
||||
}
|
||||
|
||||
Text(text = "$followCount ${stringResource(R.string.follows)}")
|
||||
Text(text = followCount)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -496,7 +537,7 @@ private fun ProfileHeader(
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(35.dp)
|
||||
.height(Size35dp)
|
||||
.padding(bottom = 3.dp)
|
||||
) {
|
||||
MessageButton(baseUser, nav)
|
||||
@@ -531,6 +572,7 @@ private fun ProfileActions(
|
||||
val account = remember(accountLocalUserState) { accountLocalUserState?.account } ?: return
|
||||
|
||||
val accountUserState by accountViewModel.account.userProfile().live().follows.observeAsState()
|
||||
val baseUserState by baseUser.live().follows.observeAsState()
|
||||
|
||||
val accountUser = remember(accountUserState) { accountUserState?.user } ?: return
|
||||
|
||||
@@ -546,7 +588,7 @@ private fun ProfileActions(
|
||||
}
|
||||
}
|
||||
|
||||
val isUserFollowingLoggedIn by remember(accountUserState, accountLocalUserState) {
|
||||
val isUserFollowingLoggedIn by remember(baseUserState, accountLocalUserState) {
|
||||
derivedStateOf {
|
||||
baseUser.isFollowing(accountUser)
|
||||
}
|
||||
@@ -874,7 +916,7 @@ private fun WatchApp(baseApp: Note, nav: (String) -> Unit) {
|
||||
Box(
|
||||
remember {
|
||||
Modifier
|
||||
.size(35.dp)
|
||||
.size(Size35dp)
|
||||
.clickable {
|
||||
nav("Note/${baseApp.idHex}")
|
||||
}
|
||||
@@ -885,7 +927,7 @@ private fun WatchApp(baseApp: Note, nav: (String) -> Unit) {
|
||||
contentDescription = null,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.size(35.dp)
|
||||
.size(Size35dp)
|
||||
.clip(shape = CircleShape)
|
||||
}
|
||||
)
|
||||
@@ -926,27 +968,37 @@ private fun DisplayBadges(
|
||||
@Composable
|
||||
private fun LoadAndRenderBadge(badgeAwardEventHex: String, nav: (String) -> Unit) {
|
||||
var baseNote by remember {
|
||||
mutableStateOf<Note?>(null)
|
||||
mutableStateOf<Note?>(LocalCache.getNoteIfExists(badgeAwardEventHex))
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = badgeAwardEventHex) {
|
||||
launch(Dispatchers.IO) {
|
||||
baseNote = LocalCache.getOrCreateNote(badgeAwardEventHex)
|
||||
if (baseNote == null) {
|
||||
launch(Dispatchers.IO) {
|
||||
baseNote = LocalCache.getOrCreateNote(badgeAwardEventHex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
baseNote?.let {
|
||||
val badgeAwardState by it.live().metadata.observeAsState()
|
||||
val baseBadgeDefinition by remember(badgeAwardState) {
|
||||
derivedStateOf {
|
||||
badgeAwardState?.note?.replyTo?.firstOrNull()
|
||||
}
|
||||
}
|
||||
ObserveAndRenderBadge(it, nav)
|
||||
}
|
||||
}
|
||||
|
||||
baseBadgeDefinition?.let {
|
||||
BadgeThumb(it, nav, 35.dp)
|
||||
@Composable
|
||||
private fun ObserveAndRenderBadge(
|
||||
it: Note,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val badgeAwardState by it.live().metadata.observeAsState()
|
||||
val baseBadgeDefinition by remember(badgeAwardState) {
|
||||
derivedStateOf {
|
||||
badgeAwardState?.note?.replyTo?.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
baseBadgeDefinition?.let {
|
||||
BadgeThumb(it, nav, Size35dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -975,41 +1027,54 @@ fun BadgeThumb(
|
||||
.height(size)
|
||||
}
|
||||
) {
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
val event = remember(noteState) { noteState?.note?.event as? BadgeDefinitionEvent } ?: return
|
||||
val image = remember(noteState) { event.thumb()?.ifBlank { null } ?: event.image()?.ifBlank { null } }
|
||||
WatchAndRenderBadgeImage(baseNote, size, pictureModifier, onClick)
|
||||
}
|
||||
}
|
||||
|
||||
if (image == null) {
|
||||
RobohashAsyncImage(
|
||||
robot = "authornotfound",
|
||||
robotSize = size,
|
||||
contentDescription = stringResource(R.string.unknown_author),
|
||||
modifier = pictureModifier
|
||||
.width(size)
|
||||
.height(size)
|
||||
.background(MaterialTheme.colors.background)
|
||||
)
|
||||
} else {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = event.id,
|
||||
robotSize = size,
|
||||
model = image,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier = pictureModifier
|
||||
.width(size)
|
||||
.height(size)
|
||||
.clip(shape = CircleShape)
|
||||
.background(MaterialTheme.colors.background)
|
||||
.run {
|
||||
if (onClick != null) {
|
||||
this.clickable(onClick = { onClick(event.id) })
|
||||
} else {
|
||||
this
|
||||
}
|
||||
@Composable
|
||||
private fun WatchAndRenderBadgeImage(
|
||||
baseNote: Note,
|
||||
size: Dp,
|
||||
pictureModifier: Modifier,
|
||||
onClick: ((String) -> Unit)?
|
||||
) {
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
val eventId = remember(noteState) { noteState?.note?.idHex } ?: return
|
||||
val image = remember(noteState) {
|
||||
val event = noteState?.note?.event as? BadgeDefinitionEvent
|
||||
event?.thumb()?.ifBlank { null } ?: event?.image()?.ifBlank { null }
|
||||
}
|
||||
|
||||
if (image == null) {
|
||||
RobohashAsyncImage(
|
||||
robot = "authornotfound",
|
||||
robotSize = size,
|
||||
contentDescription = stringResource(R.string.unknown_author),
|
||||
modifier = pictureModifier
|
||||
.width(size)
|
||||
.height(size)
|
||||
.background(MaterialTheme.colors.background)
|
||||
)
|
||||
} else {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = eventId,
|
||||
robotSize = size,
|
||||
model = image,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier = pictureModifier
|
||||
.width(size)
|
||||
.height(size)
|
||||
.clip(shape = CircleShape)
|
||||
.background(MaterialTheme.colors.background)
|
||||
.run {
|
||||
if (onClick != null) {
|
||||
this.clickable(onClick = { onClick(eventId) })
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1109,9 +1174,18 @@ fun TabFollows(baseUser: User, feedViewModel: UserFeedViewModel, accountViewMode
|
||||
WatchFollowChanges(baseUser, feedViewModel)
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
Column() {
|
||||
RefreshingFeedUserFeedView(feedViewModel, accountViewModel, nav, enablePullRefresh = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TabFollowers(baseUser: User, feedViewModel: UserFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
WatchFollowerChanges(baseUser, feedViewModel)
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column() {
|
||||
RefreshingFeedUserFeedView(feedViewModel, accountViewModel, nav, enablePullRefresh = false)
|
||||
}
|
||||
}
|
||||
@@ -1129,14 +1203,24 @@ private fun WatchFollowChanges(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WatchFollowerChanges(
|
||||
baseUser: User,
|
||||
feedViewModel: UserFeedViewModel
|
||||
) {
|
||||
val userState by baseUser.live().followers.observeAsState()
|
||||
|
||||
LaunchedEffect(userState) {
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TabReceivedZaps(baseUser: User, zapFeedViewModel: NostrUserProfileZapsFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
WatchZapsAndUpdateFeed(baseUser, zapFeedViewModel)
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
Column() {
|
||||
LnZapFeedView(zapFeedViewModel, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
@@ -1161,9 +1245,7 @@ fun TabReports(baseUser: User, accountViewModel: AccountViewModel, nav: (String)
|
||||
WatchReportsAndUpdateFeed(baseUser, feedViewModel)
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
Column() {
|
||||
RefresheableFeedView(feedViewModel, null, accountViewModel, nav, enablePullRefresh = false)
|
||||
}
|
||||
}
|
||||
@@ -1316,16 +1398,16 @@ fun ShowUserButton(onClick: () -> Unit) {
|
||||
|
||||
@Composable
|
||||
fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
DropdownMenu(
|
||||
expanded = popupExpanded,
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account!!
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())); onDismiss() }) {
|
||||
Text(stringResource(R.string.copy_user_id))
|
||||
}
|
||||
|
||||
@@ -2,14 +2,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
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
|
||||
@@ -55,9 +53,7 @@ fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, nav: (Stri
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
Column() {
|
||||
ThreadFeedView(noteId, feedViewModel, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ import com.vitorpamplona.amethyst.ui.screen.LoadingFeed
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -397,8 +398,8 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (St
|
||||
BoostReaction(baseNote, accountViewModel, iconSize = 40.dp) {
|
||||
wantsToQuote = baseNote
|
||||
}*/
|
||||
LikeReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp, heartSize = 35.dp)
|
||||
ZapReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp, animationSize = 35.dp)
|
||||
LikeReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp, heartSize = Size35dp, 28.sp)
|
||||
ZapReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp, animationSize = Size35dp)
|
||||
ViewCountReaction(baseNote.idHex, grayTint = MaterialTheme.colors.onBackground, iconSize = 40.dp, barChartSize = 39.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import java.util.*
|
||||
|
||||
@@ -275,7 +276,7 @@ fun LoginPage(
|
||||
}
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(35.dp),
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.vitorpamplona.amethyst.ui.theme
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Shapes
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val Shapes = Shapes(
|
||||
@@ -10,8 +13,15 @@ val Shapes = Shapes(
|
||||
large = RoundedCornerShape(0.dp)
|
||||
)
|
||||
|
||||
val SmallBorder = RoundedCornerShape(7.dp)
|
||||
val QuoteBorder = RoundedCornerShape(15.dp)
|
||||
val ButtonBorder = RoundedCornerShape(20.dp)
|
||||
|
||||
val ChatBubbleShapeMe = RoundedCornerShape(15.dp, 15.dp, 3.dp, 15.dp)
|
||||
val ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp)
|
||||
|
||||
val StdButtonSizeModifier = Modifier.size(20.dp)
|
||||
val StdHorzSpacer = Modifier.width(5.dp)
|
||||
val DoubleHorzSpacer = Modifier.width(10.dp)
|
||||
|
||||
val Size35dp = 35.dp
|
||||
|
||||
@@ -54,6 +54,9 @@ private val LightPlaceholderText = LightColorPalette.onSurface.copy(alpha = 0.32
|
||||
private val DarkSubtleBorder = DarkColorPalette.onSurface.copy(alpha = 0.12f)
|
||||
private val LightSubtleBorder = LightColorPalette.onSurface.copy(alpha = 0.12f)
|
||||
|
||||
private val DarkZapraiserBackground = BitcoinOrange.copy(0.52f).compositeOver(DarkColorPalette.background)
|
||||
private val LightZapraiserBackground = BitcoinOrange.copy(0.52f).compositeOver(LightColorPalette.background)
|
||||
|
||||
private val DarkImageVerifier = Nip05.copy(0.52f).compositeOver(DarkColorPalette.background)
|
||||
private val LightImageVerifier = Nip05.copy(0.52f).compositeOver(LightColorPalette.background)
|
||||
|
||||
@@ -75,6 +78,9 @@ val Colors.secondaryButtonBackground: Color
|
||||
val Colors.lessImportantLink: Color
|
||||
get() = if (isLight) LightLessImportantLink else DarkLessImportantLink
|
||||
|
||||
val Colors.zapraiserBackground: Color
|
||||
get() = if (isLight) LightZapraiserBackground else DarkZapraiserBackground
|
||||
|
||||
val Colors.mediumImportanceLink: Color
|
||||
get() = if (isLight) LightMediumImportantLink else DarkMediumImportantLink
|
||||
val Colors.veryImportantLink: Color
|
||||
|
||||
@@ -417,4 +417,10 @@
|
||||
|
||||
<string name="new_reaction_symbol">New Reaction Symbol</string>
|
||||
<string name="no_reaction_type_setup_long_press_to_change">No reaction types selected. Long Press to change</string>
|
||||
|
||||
<string name="zapraiser">Zapraiser</string>
|
||||
<string name="zapraiser_explainer">Adds a target amount of sats to raise for this post. Supporting clients may show this as a progress bar to incentivize donations</string>
|
||||
<string name="zapraiser_target_amount_in_sats">Target Amount in Sats</string>
|
||||
|
||||
<string name="sats_to_complete">Zapraiser at %1$s. %2$s sats to goal</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user