mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3be4567e2 | ||
|
|
c5a586ec56 | ||
|
|
ece3664e74 | ||
|
|
2297c797dc | ||
|
|
4517ca9fdd | ||
|
|
1146b8ad54 | ||
|
|
545643d165 | ||
|
|
359e22ae42 | ||
|
|
69be28ec98 | ||
|
|
a8ffbb922d | ||
|
|
bd4a450336 | ||
|
|
39818268ab | ||
|
|
d26b83a851 | ||
|
|
8dc9c2ed2f | ||
|
|
860f3bd096 | ||
|
|
a3adadb251 | ||
|
|
755d89dc79 | ||
|
|
fa4463f1d7 | ||
|
|
56a68b9100 | ||
|
|
fe317964b8 | ||
|
|
07ccf75d44 | ||
|
|
ff927d4830 | ||
|
|
6461c3e498 | ||
|
|
2ed5eae029 | ||
|
|
1693455d0d | ||
|
|
3bcda33fa6 | ||
|
|
9f394900a7 | ||
|
|
4f2c78e73a | ||
|
|
6a3600fd73 | ||
|
|
b40c6f1361 | ||
|
|
5ce19474ae | ||
|
|
e9bdc045ba | ||
|
|
aed7f46ca0 | ||
|
|
310a93ef29 | ||
|
|
1ccaa1a028 | ||
|
|
493888d121 | ||
|
|
e5aba62fdd | ||
|
|
b643cedf8f | ||
|
|
75d56214d2 |
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.8.10" />
|
||||
<option name="version" value="1.8.21" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -60,12 +60,12 @@ Or get the latest APK from the [Releases Section](https://github.com/vitorpamplo
|
||||
- [x] Audio Tracks (zapstr.live) (NIP-TBD)
|
||||
- [x] Push Notifications (Zaps and Messages)
|
||||
- [x] Generic Tags (NIP-12)
|
||||
- [x] Sensitive Content (NIP-36)
|
||||
- [ ] Marketplace (NIP-15)
|
||||
- [ ] Image/Video Capture in the app
|
||||
- [ ] Local Database
|
||||
- [ ] View Individual Reactions (Like, Boost, Zaps, Reports) per Post
|
||||
- [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51)
|
||||
- [ ] Sensitive Content (NIP-36)
|
||||
- [ ] Relay Pages (NIP-11)
|
||||
- [ ] Proof of Work in the Phone (NIP-13, NIP-20)
|
||||
- [ ] Events with a Subject (NIP-14)
|
||||
|
||||
+7
-5
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 175
|
||||
versionName "0.51.0"
|
||||
versionCode 182
|
||||
versionName "0.52.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@@ -24,6 +24,7 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Make sure all of JSON parsers work when activating these.
|
||||
//minifyEnabled true
|
||||
//proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
resValue "string", "app_name", "@string/app_name_release"
|
||||
@@ -71,7 +72,7 @@ android {
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion "1.4.3"
|
||||
kotlinCompilerExtensionVersion "1.4.7"
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
@@ -86,7 +87,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.activity:activity-compose:1.7.1'
|
||||
implementation 'androidx.activity:activity-compose:1.7.2'
|
||||
implementation "androidx.compose.ui:ui:$compose_ui_version"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
|
||||
|
||||
@@ -131,7 +132,8 @@ dependencies {
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||
|
||||
// link preview
|
||||
implementation 'tw.com.oneup.www:Baha-UrlPreview:1.0.1'
|
||||
implementation 'org.jsoup:jsoup:1.16.1'
|
||||
//implementation 'tw.com.oneup.www:Baha-UrlPreview:1.0.1'
|
||||
|
||||
// Encrypted Key Storage
|
||||
implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha06'
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="orientation|screenSize|screenLayout"
|
||||
android:theme="@style/Theme.Amethyst">
|
||||
|
||||
<intent-filter android:label="Amethyst">
|
||||
|
||||
@@ -58,6 +58,7 @@ private object PrefKeys {
|
||||
const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog"
|
||||
const val USE_PROXY = "use_proxy"
|
||||
const val PROXY_PORT = "proxy_port"
|
||||
const val SHOW_SENSITIVE_CONTENT = "show_sensitive_content"
|
||||
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
|
||||
}
|
||||
|
||||
@@ -214,6 +215,12 @@ object LocalPreferences {
|
||||
putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog)
|
||||
putBoolean(PrefKeys.USE_PROXY, account.proxy != null)
|
||||
putInt(PrefKeys.PROXY_PORT, account.proxyPort)
|
||||
|
||||
if (account.showSensitiveContent == null) {
|
||||
remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
|
||||
} else {
|
||||
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!)
|
||||
}
|
||||
}.apply()
|
||||
}
|
||||
|
||||
@@ -292,6 +299,12 @@ object LocalPreferences {
|
||||
val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050)
|
||||
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||
|
||||
val showSensitiveContent = if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) {
|
||||
getBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, false)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val a = Account(
|
||||
Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
|
||||
followingChannels,
|
||||
@@ -311,7 +324,8 @@ object LocalPreferences {
|
||||
hideBlockAlertDialog,
|
||||
latestContactList,
|
||||
proxy,
|
||||
proxyPort
|
||||
proxyPort,
|
||||
showSensitiveContent
|
||||
)
|
||||
|
||||
return a
|
||||
|
||||
@@ -15,7 +15,6 @@ import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.amethyst.service.relays.Constants
|
||||
|
||||
object ServiceManager {
|
||||
private var account: Account? = null
|
||||
@@ -46,9 +45,6 @@ object ServiceManager {
|
||||
NostrSingleEventDataSource.start()
|
||||
NostrSingleChannelDataSource.start()
|
||||
NostrSingleUserDataSource.start()
|
||||
} else {
|
||||
// if not logged in yet, start a basic service wit default relays
|
||||
Client.connect(Constants.convertDefaultRelays())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,8 @@ class Account(
|
||||
var hideBlockAlertDialog: Boolean = false,
|
||||
var backupContactList: ContactListEvent? = null,
|
||||
var proxy: Proxy?,
|
||||
var proxyPort: Int
|
||||
var proxyPort: Int,
|
||||
var showSensitiveContent: Boolean? = null
|
||||
) {
|
||||
var transientHiddenUsers: Set<String> = setOf()
|
||||
|
||||
@@ -480,7 +481,14 @@ class Account(
|
||||
return LocalCache.notes[signedEvent.id]
|
||||
}
|
||||
|
||||
fun sendPost(message: String, replyTo: List<Note>?, mentions: List<User>?, tags: List<String>? = null, zapReceiver: String? = null) {
|
||||
fun sendPost(
|
||||
message: String,
|
||||
replyTo: List<Note>?,
|
||||
mentions: List<User>?,
|
||||
tags: List<String>? = null,
|
||||
zapReceiver: String? = null,
|
||||
wantsToMarkAsSensitive: Boolean
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val repliesToHex = replyTo?.filter { it.address() == null }?.map { it.idHex }
|
||||
@@ -494,6 +502,7 @@ class Account(
|
||||
addresses = addresses,
|
||||
extraTags = tags,
|
||||
zapReceiver = zapReceiver,
|
||||
markAsSensitive = wantsToMarkAsSensitive,
|
||||
privateKey = loggedIn.privKey!!
|
||||
)
|
||||
|
||||
@@ -510,7 +519,8 @@ class Account(
|
||||
valueMinimum: Int?,
|
||||
consensusThreshold: Int?,
|
||||
closedAt: Int?,
|
||||
zapReceiver: String? = null
|
||||
zapReceiver: String? = null,
|
||||
wantsToMarkAsSensitive: Boolean
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
@@ -529,14 +539,15 @@ class Account(
|
||||
valueMinimum = valueMinimum,
|
||||
consensusThreshold = consensusThreshold,
|
||||
closedAt = closedAt,
|
||||
zapReceiver = zapReceiver
|
||||
zapReceiver = zapReceiver,
|
||||
markAsSensitive = wantsToMarkAsSensitive
|
||||
)
|
||||
// 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) {
|
||||
fun sendChannelMessage(message: String, toChannel: String, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
// val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
|
||||
@@ -549,13 +560,14 @@ class Account(
|
||||
replyTos = repliesToHex,
|
||||
mentions = mentionsHex,
|
||||
zapReceiver = zapReceiver,
|
||||
markAsSensitive = wantsToMarkAsSensitive,
|
||||
privateKey = loggedIn.privKey!!
|
||||
)
|
||||
Client.send(signedEvent)
|
||||
LocalCache.consume(signedEvent, null)
|
||||
}
|
||||
|
||||
fun sendPrivateMessage(message: String, toUser: String, replyingTo: Note? = null, mentions: List<User>?, zapReceiver: String? = null) {
|
||||
fun sendPrivateMessage(message: String, toUser: String, replyingTo: Note? = null, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean) {
|
||||
if (!isWriteable()) return
|
||||
val user = LocalCache.users[toUser] ?: return
|
||||
|
||||
@@ -569,6 +581,7 @@ class Account(
|
||||
replyTos = repliesToHex,
|
||||
mentions = mentionsHex,
|
||||
zapReceiver = zapReceiver,
|
||||
markAsSensitive = wantsToMarkAsSensitive,
|
||||
privateKey = loggedIn.privKey!!,
|
||||
advertiseNip18 = false
|
||||
)
|
||||
@@ -1114,6 +1127,12 @@ class Account(
|
||||
saveable.invalidateData()
|
||||
}
|
||||
|
||||
fun updateShowSensitiveContent(show: Boolean?) {
|
||||
showSensitiveContent = show
|
||||
saveable.invalidateData()
|
||||
live.invalidateData()
|
||||
}
|
||||
|
||||
fun registerObservers() {
|
||||
// Observes relays to restart connections
|
||||
userProfile().live().relays.observeForever {
|
||||
|
||||
@@ -232,7 +232,7 @@ open class Note(val idHex: String) {
|
||||
fun isZappedBy(user: User, account: Account): Boolean {
|
||||
// Zaps who the requester was the user
|
||||
return zaps.any {
|
||||
it.key.author === user || account.decryptZapContentAuthor(it.key)?.pubKey == user.pubkeyHex
|
||||
it.key.author?.pubkeyHex == user.pubkeyHex || account.decryptZapContentAuthor(it.key)?.pubKey == user.pubkeyHex
|
||||
} || zapPayments.any {
|
||||
val zapResponseEvent = it.value?.event as? LnZapPaymentResponseEvent
|
||||
val response = if (zapResponseEvent != null) {
|
||||
@@ -245,11 +245,11 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
|
||||
fun isReactedBy(user: User): Boolean {
|
||||
return reactions.any { it.author === user }
|
||||
return reactions.any { it.author?.pubkeyHex == user.pubkeyHex }
|
||||
}
|
||||
|
||||
fun isBoostedBy(user: User): Boolean {
|
||||
return boosts.any { it.author === user }
|
||||
return boosts.any { it.author?.pubkeyHex == user.pubkeyHex }
|
||||
}
|
||||
|
||||
fun reportsBy(user: User): Set<Note> {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import com.baha.url.preview.BahaUrlPreview
|
||||
import com.baha.url.preview.IUrlPreviewCallback
|
||||
import com.baha.url.preview.UrlInfoItem
|
||||
import com.vitorpamplona.amethyst.service.previews.BahaUrlPreview
|
||||
import com.vitorpamplona.amethyst.service.previews.IUrlPreviewCallback
|
||||
import com.vitorpamplona.amethyst.service.previews.UrlInfoItem
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
@@ -299,7 +299,7 @@ class User(val pubkeyHex: String) {
|
||||
fun hasSentMessagesTo(user: User?): Boolean {
|
||||
val messagesToUser = privateChatrooms[user] ?: return false
|
||||
|
||||
return messagesToUser.roomMessages.any { this === it.author }
|
||||
return messagesToUser.roomMessages.any { this.pubkeyHex == it.author?.pubkeyHex }
|
||||
}
|
||||
|
||||
fun hasReport(loggedIn: User, type: ReportEvent.ReportType): Boolean {
|
||||
|
||||
@@ -22,7 +22,16 @@ class ChannelMessageEvent(
|
||||
companion object {
|
||||
const val kind = 42
|
||||
|
||||
fun create(message: String, channel: String, replyTos: List<String>? = null, mentions: List<String>? = null, zapReceiver: String?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMessageEvent {
|
||||
fun create(
|
||||
message: String,
|
||||
channel: String,
|
||||
replyTos: List<String>? = null,
|
||||
mentions: List<String>? = null,
|
||||
zapReceiver: String?,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000,
|
||||
markAsSensitive: Boolean
|
||||
): ChannelMessageEvent {
|
||||
val content = message
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val tags = mutableListOf(
|
||||
@@ -37,6 +46,9 @@ class ChannelMessageEvent(
|
||||
zapReceiver?.let {
|
||||
tags.add(listOf("zap", it))
|
||||
}
|
||||
if (markAsSensitive) {
|
||||
tags.add(listOf("content-warning", ""))
|
||||
}
|
||||
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
|
||||
@@ -48,6 +48,12 @@ open class Event(
|
||||
|
||||
fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] }
|
||||
|
||||
override fun isSensitive() = tags.any {
|
||||
(it.size > 0 && it[0].equals("content-warning", true)) ||
|
||||
(it.size > 1 && it[0] == "t" && it[1].equals("nsfw", true)) ||
|
||||
(it.size > 1 && it[0] == "t" && it[1].equals("nude", true))
|
||||
}
|
||||
|
||||
override fun zapAddress() = tags.firstOrNull { it.size > 1 && it[0] == "zap" }?.get(1)
|
||||
|
||||
fun taggedAddresses() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull {
|
||||
|
||||
@@ -37,4 +37,5 @@ interface EventInterface {
|
||||
fun getPoWRank(): Int
|
||||
|
||||
fun zapAddress(): String?
|
||||
fun isSensitive(): Boolean
|
||||
}
|
||||
|
||||
@@ -51,7 +51,8 @@ class PollNoteEvent(
|
||||
valueMinimum: Int?,
|
||||
consensusThreshold: Int?,
|
||||
closedAt: Int?,
|
||||
zapReceiver: String?
|
||||
zapReceiver: String?,
|
||||
markAsSensitive: Boolean
|
||||
): PollNoteEvent {
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val tags = mutableListOf<List<String>>()
|
||||
@@ -75,6 +76,9 @@ class PollNoteEvent(
|
||||
if (zapReceiver != null) {
|
||||
tags.add(listOf("zap", zapReceiver))
|
||||
}
|
||||
if (markAsSensitive) {
|
||||
tags.add(listOf("content-warning", ""))
|
||||
}
|
||||
|
||||
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
|
||||
@@ -39,7 +39,7 @@ class PrivateDmEvent(
|
||||
|
||||
fun with(pubkeyHex: String): Boolean {
|
||||
return pubkeyHex == pubKey ||
|
||||
tags.firstOrNull { it.size > 1 && it[0] == "p" }?.getOrNull(1) == pubkeyHex
|
||||
tags.any { it.size > 1 && it[0] == "p" && it[1] == pubkeyHex }
|
||||
}
|
||||
|
||||
fun plainContent(privKey: ByteArray, pubKey: ByteArray): String? {
|
||||
@@ -73,7 +73,8 @@ class PrivateDmEvent(
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000,
|
||||
publishedRecipientPubKey: ByteArray? = null,
|
||||
advertiseNip18: Boolean = true
|
||||
advertiseNip18: Boolean = true,
|
||||
markAsSensitive: Boolean
|
||||
): PrivateDmEvent {
|
||||
val content = Utils.encrypt(
|
||||
if (advertiseNip18) { nip18Advertisement } else { "" } + msg,
|
||||
@@ -94,6 +95,9 @@ class PrivateDmEvent(
|
||||
zapReceiver?.let {
|
||||
tags.add(listOf("zap", it))
|
||||
}
|
||||
if (markAsSensitive) {
|
||||
tags.add(listOf("content-warning", ""))
|
||||
}
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
return PrivateDmEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||
|
||||
@@ -29,6 +29,7 @@ class TextNoteEvent(
|
||||
addresses: List<ATag>?,
|
||||
extraTags: List<String>?,
|
||||
zapReceiver: String?,
|
||||
markAsSensitive: Boolean,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000
|
||||
): TextNoteEvent {
|
||||
@@ -56,6 +57,9 @@ class TextNoteEvent(
|
||||
findURLs(msg).forEach {
|
||||
tags.add(listOf("r", it))
|
||||
}
|
||||
if (markAsSensitive) {
|
||||
tags.add(listOf("content-warning", ""))
|
||||
}
|
||||
|
||||
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.vitorpamplona.amethyst.service.previews
|
||||
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
|
||||
class BahaUrlPreview(val url: String, var callback: IUrlPreviewCallback?) {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
private val imageExtensionArray = arrayOf(".gif", ".png", ".jpg", ".jpeg", ".bmp", ".webp")
|
||||
|
||||
fun fetchUrlPreview(timeOut: Int = 30000) {
|
||||
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
|
||||
callback?.onFailed(throwable)
|
||||
}
|
||||
scope.launch(exceptionHandler) {
|
||||
fetch(timeOut)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetch(timeOut: Int = 30000) {
|
||||
lateinit var urlInfoItem: UrlInfoItem
|
||||
if (checkIsImageUrl()) {
|
||||
urlInfoItem = UrlInfoItem(url = url, image = url)
|
||||
} else {
|
||||
val document = getDocument(url, timeOut)
|
||||
urlInfoItem = parseHtml(document)
|
||||
urlInfoItem.url = url
|
||||
}
|
||||
callback?.onComplete(urlInfoItem)
|
||||
}
|
||||
|
||||
private fun checkIsImageUrl(): Boolean {
|
||||
val uri = Uri.parse(url)
|
||||
var isImage = false
|
||||
for (imageExtension in imageExtensionArray) {
|
||||
if (uri.path != null && uri.path!!.toLowerCase(Locale.getDefault()).endsWith(imageExtension)) {
|
||||
isImage = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return isImage
|
||||
}
|
||||
|
||||
fun cleanUp() {
|
||||
scope.cancel()
|
||||
callback = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.service.previews
|
||||
|
||||
interface IUrlPreviewCallback {
|
||||
fun onComplete(urlInfo: UrlInfoItem)
|
||||
fun onFailed(throwable: Throwable)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.vitorpamplona.amethyst.service.previews
|
||||
|
||||
data class UrlInfoItem(
|
||||
var url: String = "",
|
||||
var title: String = "",
|
||||
var description: String = "",
|
||||
var image: String = ""
|
||||
) {
|
||||
fun allFetchComplete(): Boolean {
|
||||
return title.isNotEmpty() && description.isNotEmpty() && image.isNotEmpty()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.vitorpamplona.amethyst.service.previews
|
||||
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
private const val ELEMENT_TAG_META = "meta"
|
||||
private const val ATTRIBUTE_VALUE_PROPERTY = "property"
|
||||
private const val ATTRIBUTE_VALUE_NAME = "name"
|
||||
private const val ATTRIBUTE_VALUE_ITEMPROP = "itemprop"
|
||||
|
||||
/* for <meta property="og:" to get title */
|
||||
private val META_OG_TITLE = arrayOf("og:title", "\"og:title\"", "'og:title'")
|
||||
|
||||
/* for <meta property="og:" to get description */
|
||||
private val META_OG_DESCRIPTION =
|
||||
arrayOf("og:description", "\"og:description\"", "'og:description'")
|
||||
|
||||
/* for <meta property="og:" to get image */
|
||||
private val META_OG_IMAGE = arrayOf("og:image", "\"og:image\"", "'og:image'")
|
||||
|
||||
/*for <meta name=... to get title */
|
||||
private val META_NAME_TITLE = arrayOf(
|
||||
"twitter:title",
|
||||
"\"twitter:title\"",
|
||||
"'twitter:title'",
|
||||
"title",
|
||||
"\"title\"",
|
||||
"'title'"
|
||||
)
|
||||
|
||||
/*for <meta name=... to get description */
|
||||
private val META_NAME_DESCRIPTION = arrayOf(
|
||||
"twitter:description",
|
||||
"\"twitter:description\"",
|
||||
"'twitter:description'",
|
||||
"description",
|
||||
"\"description\"",
|
||||
"'description'"
|
||||
)
|
||||
|
||||
/*for <meta name=... to get image */
|
||||
private val META_NAME_IMAGE = arrayOf(
|
||||
"twitter:image",
|
||||
"\"twitter:image\"",
|
||||
"'twitter:image'"
|
||||
)
|
||||
|
||||
/*for <meta itemprop=... to get title */
|
||||
private val META_ITEMPROP_TITLE = arrayOf("name", "\"name\"", "'name'")
|
||||
|
||||
/*for <meta itemprop=... to get description */
|
||||
private val META_ITEMPROP_DESCRIPTION = arrayOf("description", "\"description\"", "'description'")
|
||||
|
||||
/*for <meta itemprop=... to get image */
|
||||
private val META_ITEMPROP_IMAGE = arrayOf("image", "\"image\"", "'image'")
|
||||
|
||||
private const val CONTENT = "content"
|
||||
|
||||
suspend fun getDocument(url: String, timeOut: Int = 30000): Document =
|
||||
withContext(Dispatchers.IO) {
|
||||
return@withContext Jsoup.connect(url)
|
||||
.proxy(HttpClient.getProxy())
|
||||
.timeout(timeOut)
|
||||
.get()
|
||||
}
|
||||
|
||||
suspend fun parseHtml(document: Document): UrlInfoItem =
|
||||
withContext(Dispatchers.IO) {
|
||||
val metaTags = document.getElementsByTag(ELEMENT_TAG_META)
|
||||
val urlInfo = UrlInfoItem()
|
||||
metaTags.forEach {
|
||||
val propertyTag = it.attr(ATTRIBUTE_VALUE_PROPERTY)
|
||||
when (propertyTag) {
|
||||
in META_OG_TITLE -> if (urlInfo.title.isEmpty()) urlInfo.title = it.attr(CONTENT)
|
||||
in META_OG_DESCRIPTION -> if (urlInfo.description.isEmpty()) {
|
||||
urlInfo.description =
|
||||
it.attr(CONTENT)
|
||||
}
|
||||
in META_OG_IMAGE -> if (urlInfo.image.isEmpty()) urlInfo.image = it.attr(CONTENT)
|
||||
}
|
||||
|
||||
when (it.attr(ATTRIBUTE_VALUE_NAME)) {
|
||||
in META_NAME_TITLE -> if (urlInfo.title.isEmpty()) urlInfo.title = it.attr(CONTENT)
|
||||
in META_NAME_DESCRIPTION -> if (urlInfo.description.isEmpty()) {
|
||||
urlInfo.description =
|
||||
it.attr(CONTENT)
|
||||
}
|
||||
in META_OG_IMAGE -> if (urlInfo.image.isEmpty()) urlInfo.image = it.attr(CONTENT)
|
||||
}
|
||||
|
||||
when (it.attr(ATTRIBUTE_VALUE_ITEMPROP)) {
|
||||
in META_ITEMPROP_TITLE -> if (urlInfo.title.isEmpty()) {
|
||||
urlInfo.title =
|
||||
it.attr(CONTENT)
|
||||
}
|
||||
in META_ITEMPROP_DESCRIPTION -> if (urlInfo.description.isEmpty()) {
|
||||
urlInfo.description =
|
||||
it.attr(
|
||||
CONTENT
|
||||
)
|
||||
}
|
||||
in META_ITEMPROP_IMAGE -> if (urlInfo.image.isEmpty()) {
|
||||
urlInfo.image = it.attr(
|
||||
CONTENT
|
||||
)
|
||||
}
|
||||
}
|
||||
if (urlInfo.allFetchComplete()) {
|
||||
return@withContext urlInfo
|
||||
}
|
||||
}
|
||||
return@withContext urlInfo
|
||||
}
|
||||
@@ -22,8 +22,12 @@ 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.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material.icons.outlined.ArrowForwardIos
|
||||
import androidx.compose.material.icons.outlined.Bolt
|
||||
import androidx.compose.material.icons.rounded.Warning
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -83,7 +87,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
val scroolState = rememberScrollState()
|
||||
val scrollState = rememberScrollState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -153,7 +157,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(scroolState)
|
||||
.verticalScroll(scrollState)
|
||||
) {
|
||||
Notifying(postViewModel.mentions) {
|
||||
postViewModel.removeFromReplyList(it)
|
||||
@@ -335,10 +339,11 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
||||
}
|
||||
|
||||
if (postViewModel.canUsePoll) {
|
||||
val hashtag = stringResource(R.string.poll_hashtag)
|
||||
// These should be hashtag recommendations the user selects in the future.
|
||||
// val hashtag = stringResource(R.string.poll_hashtag)
|
||||
// postViewModel.includePollHashtagInMessage(postViewModel.wantsPoll, hashtag)
|
||||
AddPollButton(postViewModel.wantsPoll) {
|
||||
postViewModel.wantsPoll = !postViewModel.wantsPoll
|
||||
postViewModel.includePollHashtagInMessage(postViewModel.wantsPoll, hashtag)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,6 +353,10 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
||||
}
|
||||
}
|
||||
|
||||
MarkAsSensitive(postViewModel) {
|
||||
postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive
|
||||
}
|
||||
|
||||
ForwardZapTo(postViewModel) {
|
||||
postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo
|
||||
}
|
||||
@@ -537,6 +546,60 @@ private fun ForwardZapTo(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarkAsSensitive(
|
||||
postViewModel: NewPostViewModel,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onClick()
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.height(20.dp)
|
||||
.width(23.dp)
|
||||
) {
|
||||
if (!postViewModel.wantsToMarkAsSensitive) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Visibility,
|
||||
contentDescription = stringResource(R.string.content_warning),
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.align(Alignment.BottomStart),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Warning,
|
||||
contentDescription = stringResource(R.string.content_warning),
|
||||
modifier = Modifier
|
||||
.size(10.dp)
|
||||
.align(Alignment.TopEnd),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.VisibilityOff,
|
||||
contentDescription = stringResource(id = R.string.content_warning),
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.align(Alignment.BottomStart),
|
||||
tint = Color.Red
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Warning,
|
||||
contentDescription = stringResource(id = R.string.content_warning),
|
||||
modifier = Modifier
|
||||
.size(10.dp)
|
||||
.align(Alignment.TopEnd),
|
||||
tint = Color.Yellow
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CloseButton(onCancel: () -> Unit) {
|
||||
Button(
|
||||
|
||||
@@ -67,6 +67,9 @@ open class NewPostViewModel : ViewModel() {
|
||||
var forwardZapTo by mutableStateOf<User?>(null)
|
||||
var forwardZapToEditting by mutableStateOf(TextFieldValue(""))
|
||||
|
||||
// NSFW, Sensitive
|
||||
var wantsToMarkAsSensitive by mutableStateOf(false)
|
||||
|
||||
open fun load(account: Account, replyingTo: Note?, quote: Note?) {
|
||||
originalNote = replyingTo
|
||||
replyingTo?.let { replyNote ->
|
||||
@@ -97,6 +100,7 @@ open class NewPostViewModel : ViewModel() {
|
||||
contentToAddUrl = null
|
||||
|
||||
wantsForwardZapTo = false
|
||||
wantsToMarkAsSensitive = false
|
||||
forwardZapTo = null
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
|
||||
@@ -118,13 +122,13 @@ open class NewPostViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
if (wantsPoll) {
|
||||
account?.sendPoll(tagger.message, tagger.replyTos, tagger.mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt, zapReceiver)
|
||||
account?.sendPoll(tagger.message, tagger.replyTos, tagger.mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt, zapReceiver, wantsToMarkAsSensitive)
|
||||
} else if (originalNote?.channel() != null) {
|
||||
account?.sendChannelMessage(tagger.message, tagger.channel!!.idHex, tagger.replyTos, tagger.mentions, zapReceiver)
|
||||
account?.sendChannelMessage(tagger.message, tagger.channel!!.idHex, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive)
|
||||
} else if (originalNote?.event is PrivateDmEvent) {
|
||||
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!.pubkeyHex, originalNote!!, tagger.mentions, zapReceiver)
|
||||
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!.pubkeyHex, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive)
|
||||
} else {
|
||||
account?.sendPost(tagger.message, tagger.replyTos, tagger.mentions, null, zapReceiver)
|
||||
account?.sendPost(tagger.message, tagger.replyTos, tagger.mentions, null, zapReceiver, wantsToMarkAsSensitive)
|
||||
}
|
||||
|
||||
cancel()
|
||||
@@ -183,6 +187,7 @@ open class NewPostViewModel : ViewModel() {
|
||||
wantsInvoice = false
|
||||
|
||||
wantsForwardZapTo = false
|
||||
wantsToMarkAsSensitive = false
|
||||
forwardZapTo = null
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ private fun DisplayEvent(
|
||||
nip19: Nip19.Return,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var noteBase by remember { mutableStateOf<Note?>(null) }
|
||||
var noteBase by remember(nip19) { mutableStateOf<Note?>(null) }
|
||||
|
||||
LaunchedEffect(key1 = nip19.hex) {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -129,7 +129,7 @@ private fun DisplayNote(
|
||||
nip19: Nip19.Return,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var noteBase by remember { mutableStateOf<Note?>(null) }
|
||||
var noteBase by remember(nip19) { mutableStateOf<Note?>(null) }
|
||||
|
||||
LaunchedEffect(key1 = nip19.hex) {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -185,7 +185,7 @@ private fun DisplayAddress(
|
||||
nip19: Nip19.Return,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var noteBase by remember { mutableStateOf<Note?>(null) }
|
||||
var noteBase by remember(nip19) { mutableStateOf<Note?>(null) }
|
||||
|
||||
LaunchedEffect(key1 = nip19.hex) {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -217,7 +217,7 @@ private fun DisplayUser(
|
||||
nip19: Nip19.Return,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var userBase by remember { mutableStateOf<User?>(null) }
|
||||
var userBase by remember(nip19) { mutableStateOf<User?>(null) }
|
||||
|
||||
LaunchedEffect(key1 = nip19.hex) {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -227,7 +227,7 @@ private fun DisplayUser(
|
||||
|
||||
userBase?.let {
|
||||
val userState by it.live().metadata.observeAsState()
|
||||
val route = remember { "User/${it.pubkeyHex}" }
|
||||
val route = remember(userState) { "User/${it.pubkeyHex}" }
|
||||
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
|
||||
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags }
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ fun RichTextViewer(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val isMarkdown = remember { isMarkdown(content) }
|
||||
val isMarkdown = remember(content) { isMarkdown(content) }
|
||||
|
||||
Column(modifier = modifier) {
|
||||
if (isMarkdown) {
|
||||
@@ -124,7 +124,7 @@ private fun RenderRegular(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var state by remember {
|
||||
var state by remember(content) {
|
||||
mutableStateOf(RichTextViewerState(content, emptySet(), emptyMap(), emptyList(), emptyMap()))
|
||||
}
|
||||
|
||||
@@ -344,9 +344,9 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color, tag
|
||||
)
|
||||
)
|
||||
|
||||
var markdownWithSpecialContent by remember { mutableStateOf<String?>(null) }
|
||||
var nip19References by remember { mutableStateOf<List<Nip19.Return>>(emptyList()) }
|
||||
var refresh by remember { mutableStateOf(0) }
|
||||
var markdownWithSpecialContent by remember(content) { mutableStateOf<String?>(null) }
|
||||
var nip19References by remember(content) { mutableStateOf<List<Nip19.Return>>(emptyList()) }
|
||||
var refresh by remember(content) { mutableStateOf(0) }
|
||||
|
||||
LaunchedEffect(key1 = content) {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -365,8 +365,8 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color, tag
|
||||
}
|
||||
|
||||
nip19References.forEach {
|
||||
var baseUser by remember { mutableStateOf<User?>(null) }
|
||||
var baseNote by remember { mutableStateOf<Note?>(null) }
|
||||
var baseUser by remember(it) { mutableStateOf<User?>(null) }
|
||||
var baseNote by remember(it) { mutableStateOf<Note?>(null) }
|
||||
|
||||
LaunchedEffect(key1 = it.hex) {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
||||
@@ -34,7 +34,9 @@ fun RobohashAsyncImage(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val size = with(LocalDensity.current) {
|
||||
robotSize.roundToPx()
|
||||
remember {
|
||||
robotSize.roundToPx()
|
||||
}
|
||||
}
|
||||
|
||||
val imageRequest = remember(robotSize, robot) {
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.rounded.Warning
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
fun SensitivityWarning(
|
||||
hasSensitiveContent: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
|
||||
var showContentWarningNote by remember(accountState) {
|
||||
mutableStateOf(accountState?.account?.showSensitiveContent != true && hasSensitiveContent)
|
||||
}
|
||||
|
||||
if (showContentWarningNote) {
|
||||
ContentWarningNote() {
|
||||
showContentWarningNote = false
|
||||
}
|
||||
} else {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContentWarningNote(onDismiss: () -> Unit) {
|
||||
Column() {
|
||||
Row(modifier = Modifier.padding(horizontal = 12.dp)) {
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
||||
Box(
|
||||
Modifier
|
||||
.height(80.dp)
|
||||
.width(90.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Visibility,
|
||||
contentDescription = stringResource(R.string.content_warning),
|
||||
modifier = Modifier
|
||||
.size(70.dp)
|
||||
.align(Alignment.BottomStart),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Warning,
|
||||
contentDescription = stringResource(R.string.content_warning),
|
||||
modifier = Modifier
|
||||
.size(30.dp)
|
||||
.align(Alignment.TopEnd),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
||||
Text(
|
||||
text = stringResource(R.string.content_warning),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
|
||||
Row() {
|
||||
Text(
|
||||
text = stringResource(R.string.content_warning_explanation),
|
||||
color = Color.Gray,
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
||||
Button(
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
onClick = onDismiss,
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.show_anyway),
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,10 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.baha.url.preview.IUrlPreviewCallback
|
||||
import com.baha.url.preview.UrlInfoItem
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
||||
import com.vitorpamplona.amethyst.service.previews.IUrlPreviewCallback
|
||||
import com.vitorpamplona.amethyst.service.previews.UrlInfoItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -21,7 +21,7 @@ import kotlinx.coroutines.launch
|
||||
fun UrlPreview(url: String, urlText: String) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var urlPreviewState by remember {
|
||||
var urlPreviewState by remember(url) {
|
||||
val default = UrlCachedPreviewer.cache[url]?.let {
|
||||
if (it.allFetchComplete() && it.url == url) {
|
||||
UrlPreviewState.Loaded(it)
|
||||
|
||||
@@ -20,8 +20,8 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.baha.url.preview.UrlInfoItem
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.previews.UrlInfoItem
|
||||
import java.net.URL
|
||||
|
||||
@Composable
|
||||
@@ -42,10 +42,10 @@ fun UrlPreviewCard(
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
val validatedUrl = remember { URL(previewInfo.url) }
|
||||
val validatedUrl = remember(url) { URL(previewInfo.url) }
|
||||
|
||||
// correctly treating relative images
|
||||
val imageUrl = remember {
|
||||
val imageUrl = remember(url) {
|
||||
if (previewInfo.image.startsWith("/")) {
|
||||
URL(validatedUrl, previewInfo.image).toString()
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import com.baha.url.preview.UrlInfoItem
|
||||
import com.vitorpamplona.amethyst.service.previews.UrlInfoItem
|
||||
|
||||
sealed class UrlPreviewState {
|
||||
object Loading : UrlPreviewState()
|
||||
|
||||
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.model.*
|
||||
|
||||
object NotificationFeedFilter : AdditiveFeedFilter<Note>() {
|
||||
@@ -36,7 +36,7 @@ object NotificationFeedFilter : AdditiveFeedFilter<Note>() {
|
||||
(isGlobal || it.author?.pubkeyHex in followingKeySet) &&
|
||||
it.event?.isTaggedUser(loggedInUserHex) ?: false &&
|
||||
(it.author == null || !account.isHidden(it.author!!.pubkeyHex)) &&
|
||||
tagsAnEventByUser(it, loggedInUser)
|
||||
tagsAnEventByUser(it, loggedInUserHex)
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
@@ -44,27 +44,27 @@ object NotificationFeedFilter : AdditiveFeedFilter<Note>() {
|
||||
return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
|
||||
}
|
||||
|
||||
fun tagsAnEventByUser(note: Note, author: User): Boolean {
|
||||
fun tagsAnEventByUser(note: Note, authorHex: HexKey): Boolean {
|
||||
val event = note.event
|
||||
|
||||
if (event is BaseTextNoteEvent) {
|
||||
val isAuthoredPostCited = event.findCitations().any {
|
||||
LocalCache.notes[it]?.author === author || LocalCache.addressables[it]?.author === author
|
||||
LocalCache.notes[it]?.author?.pubkeyHex == authorHex || LocalCache.addressables[it]?.author?.pubkeyHex == authorHex
|
||||
}
|
||||
|
||||
return isAuthoredPostCited ||
|
||||
(
|
||||
event.citedUsers().contains(author.pubkeyHex) ||
|
||||
note.replyTo?.any { it.author === author } == true
|
||||
event.citedUsers().contains(authorHex) ||
|
||||
note.replyTo?.any { it.author?.pubkeyHex == authorHex } == true
|
||||
)
|
||||
}
|
||||
|
||||
if (event is ReactionEvent) {
|
||||
return note.replyTo?.lastOrNull()?.author === author
|
||||
return note.replyTo?.lastOrNull()?.author?.pubkeyHex == authorHex
|
||||
}
|
||||
|
||||
if (event is RepostEvent) {
|
||||
return note.replyTo?.lastOrNull()?.author === author
|
||||
return note.replyTo?.lastOrNull()?.author?.pubkeyHex == authorHex
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
@@ -123,7 +123,6 @@ private fun RowScope.HasNewItemsIcon(
|
||||
LaunchedEffect(key1 = notifState, key2 = accountState) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val newHasNewItems = route.hasNewItems(account, notif, emptySet())
|
||||
println("Notification Change ${route.route} $hasNewItems -> $newHasNewItems")
|
||||
if (newHasNewItems != hasNewItems) {
|
||||
hasNewItems = newHasNewItems
|
||||
}
|
||||
@@ -134,7 +133,6 @@ private fun RowScope.HasNewItemsIcon(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect {
|
||||
val newHasNewItems = route.hasNewItems(account, notif, it)
|
||||
println("Notification From Base ${route.route} $hasNewItems -> $newHasNewItems")
|
||||
if (newHasNewItems != hasNewItems) {
|
||||
hasNewItems = newHasNewItems
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThreadScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.VideoScreen
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@@ -103,8 +104,14 @@ fun AppNavigation(
|
||||
)
|
||||
|
||||
// Avoids running scroll to top when back button is pressed
|
||||
// Changes this on a thread to avoid changing before it finishes the composition
|
||||
if (scrollToTop) {
|
||||
it.arguments?.remove("scrollToTop")
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
scope.launch {
|
||||
delay(1000)
|
||||
it.arguments?.remove("scrollToTop")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -121,8 +128,14 @@ fun AppNavigation(
|
||||
)
|
||||
|
||||
// Avoids running scroll to top when back button is pressed
|
||||
// Changes this on a thread to avoid changing before it finishes the composition
|
||||
if (scrollToTop) {
|
||||
it.arguments?.remove("scrollToTop")
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
scope.launch {
|
||||
delay(1000)
|
||||
it.arguments?.remove("scrollToTop")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -144,10 +157,20 @@ fun AppNavigation(
|
||||
|
||||
// Avoids running scroll to top when back button is pressed
|
||||
if (scrollToTop) {
|
||||
it.arguments?.remove("scrollToTop")
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
scope.launch {
|
||||
delay(1000)
|
||||
it.arguments?.remove("scrollToTop")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nip47 != null) {
|
||||
it.arguments?.remove("nip47")
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
scope.launch {
|
||||
delay(1000)
|
||||
it.arguments?.remove("nip47")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -165,8 +188,14 @@ fun AppNavigation(
|
||||
)
|
||||
|
||||
// Avoids running scroll to top when back button is pressed
|
||||
// Changes this on a thread to avoid changing before it finishes the composition
|
||||
if (scrollToTop) {
|
||||
it.arguments?.remove("scrollToTop")
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
scope.launch {
|
||||
delay(1000)
|
||||
it.arguments?.remove("scrollToTop")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -55,10 +55,11 @@ class AddBountyAmountViewModel : ViewModel() {
|
||||
|
||||
if (newValue != null) {
|
||||
account?.sendPost(
|
||||
newValue.toString(),
|
||||
listOfNotNull(bounty),
|
||||
listOfNotNull(bounty?.author),
|
||||
listOf("bounty-added-reward")
|
||||
message = newValue.toString(),
|
||||
replyTo = listOfNotNull(bounty),
|
||||
mentions = listOfNotNull(bounty?.author),
|
||||
tags = listOf("bounty-added-reward"),
|
||||
wantsToMarkAsSensitive = false
|
||||
)
|
||||
|
||||
nextAmount = TextFieldValue("")
|
||||
|
||||
@@ -37,8 +37,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.ColorMatrix
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
@@ -63,8 +61,10 @@ 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.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -117,7 +117,7 @@ fun ChatroomMessageCompose(
|
||||
LaunchedEffect(key1 = noteReportsState, key2 = accountState) {
|
||||
withContext(Dispatchers.IO) {
|
||||
account.userProfile().let { loggedIn ->
|
||||
val newCanPreview = note.author === loggedIn ||
|
||||
val newCanPreview = note.author?.pubkeyHex == loggedIn.pubkeyHex ||
|
||||
(note.author?.let { loggedIn.isFollowingCached(it) } ?: true) ||
|
||||
!(noteForReports.hasAnyReports())
|
||||
|
||||
@@ -369,15 +369,21 @@ private fun RenderRegularTextNote(
|
||||
val modifier = remember { Modifier.padding(top = 5.dp) }
|
||||
|
||||
if (eventContent != null) {
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
canPreview = canPreview,
|
||||
modifier = modifier,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundBubbleColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
|
||||
SensitivityWarning(
|
||||
hasSensitiveContent = hasSensitiveContent,
|
||||
accountViewModel = accountViewModel
|
||||
) {
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
canPreview = canPreview,
|
||||
modifier = modifier,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundBubbleColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
} else {
|
||||
TranslatableRichTextViewer(
|
||||
content = stringResource(id = R.string.could_not_decrypt_the_message),
|
||||
@@ -531,31 +537,27 @@ private fun RelayBadges(baseNote: Note) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderRelay(dirtyUrl: String) {
|
||||
fun RenderRelay(dirtyUrl: String) {
|
||||
val uri = LocalUriHandler.current
|
||||
val website = remember {
|
||||
val cleanUrl = dirtyUrl.removePrefix("wss://").removePrefix("ws://")
|
||||
val website = remember(dirtyUrl) {
|
||||
val cleanUrl = dirtyUrl.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/")
|
||||
"https://$cleanUrl"
|
||||
}
|
||||
val iconUrl = remember {
|
||||
val cleanUrl = dirtyUrl.removePrefix("wss://").removePrefix("ws://")
|
||||
val iconUrl = remember(dirtyUrl) {
|
||||
val cleanUrl = dirtyUrl.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/")
|
||||
"https://$cleanUrl/favicon.ico"
|
||||
}
|
||||
|
||||
val clickableModifier = remember {
|
||||
val clickableModifier = remember(dirtyUrl) {
|
||||
Modifier
|
||||
.size(15.dp)
|
||||
.padding(1.dp)
|
||||
.size(15.dp)
|
||||
.clickable(onClick = { uri.openUri(website) })
|
||||
}
|
||||
|
||||
val colorFilter = remember {
|
||||
ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0.5f) })
|
||||
}
|
||||
|
||||
val iconModifier = remember {
|
||||
val iconModifier = remember(dirtyUrl) {
|
||||
Modifier
|
||||
.size(15.dp)
|
||||
.size(13.dp)
|
||||
.clip(shape = CircleShape)
|
||||
}
|
||||
|
||||
@@ -564,10 +566,10 @@ private fun RenderRelay(dirtyUrl: String) {
|
||||
) {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = iconUrl,
|
||||
robotSize = 15.dp,
|
||||
robotSize = 13.dp,
|
||||
model = iconUrl,
|
||||
contentDescription = stringResource(id = R.string.relay_icon),
|
||||
colorFilter = colorFilter,
|
||||
colorFilter = RelayIconFilter,
|
||||
modifier = iconModifier.background(MaterialTheme.colors.background)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,9 +35,14 @@ import kotlinx.coroutines.launch
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val noteState by messageSetCard.note.live().metadata.observeAsState()
|
||||
val baseNote = remember { messageSetCard.note }
|
||||
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
val note = remember(noteState) { noteState?.note }
|
||||
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return
|
||||
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -78,8 +83,8 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
routeFor(
|
||||
note,
|
||||
accountViewModel.userProfile()
|
||||
baseNote,
|
||||
loggedIn
|
||||
)?.let { nav(it) }
|
||||
}
|
||||
},
|
||||
@@ -90,18 +95,11 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
||||
|
||||
Column(columnModifier) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Box(modifier = remember { Modifier.width(55.dp).padding(top = 5.dp, end = 5.dp) }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_dm),
|
||||
null,
|
||||
modifier = remember { Modifier.size(16.dp).align(Alignment.TopEnd) },
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
MessageIcon()
|
||||
|
||||
Column(modifier = remember { Modifier.padding(start = 10.dp) }) {
|
||||
NoteCompose(
|
||||
baseNote = messageSetCard.note,
|
||||
baseNote = baseNote,
|
||||
routeForLastRead = null,
|
||||
isBoostedNote = true,
|
||||
addMarginTop = false,
|
||||
@@ -116,3 +114,25 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageIcon() {
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(55.dp)
|
||||
.padding(top = 5.dp, end = 5.dp)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_dm),
|
||||
null,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.size(16.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
},
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
@@ -19,7 +21,6 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
@@ -29,6 +30,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -39,11 +41,9 @@ import androidx.compose.ui.unit.sp
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
@@ -55,7 +55,6 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
@@ -66,7 +65,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
||||
val note = remember(noteState) { noteState?.note } ?: return
|
||||
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return
|
||||
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -92,55 +91,48 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
||||
val primaryColor = MaterialTheme.colors.primary.copy(0.12f)
|
||||
val defaultBackgroundColor = MaterialTheme.colors.background
|
||||
|
||||
val backgroundColor by remember(isNew) {
|
||||
derivedStateOf {
|
||||
if (isNew) {
|
||||
primaryColor.compositeOver(defaultBackgroundColor)
|
||||
} else {
|
||||
defaultBackgroundColor
|
||||
}
|
||||
}
|
||||
val backgroundColor = if (isNew) {
|
||||
primaryColor.compositeOver(defaultBackgroundColor)
|
||||
} else {
|
||||
defaultBackgroundColor
|
||||
}
|
||||
|
||||
val columnModifier by remember(isNew, backgroundColor) {
|
||||
derivedStateOf {
|
||||
Modifier
|
||||
.background(backgroundColor)
|
||||
.padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 10.dp
|
||||
)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
routeFor(
|
||||
baseNote,
|
||||
account.userProfile()
|
||||
)?.let { nav(it) }
|
||||
}
|
||||
},
|
||||
onLongClick = { popupExpanded = true }
|
||||
)
|
||||
.fillMaxWidth()
|
||||
}
|
||||
}
|
||||
val columnModifier = Modifier
|
||||
.background(backgroundColor)
|
||||
.padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 10.dp
|
||||
)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
routeFor(baseNote, loggedIn)?.let { nav(it) }
|
||||
}
|
||||
},
|
||||
onLongClick = { popupExpanded = true }
|
||||
)
|
||||
.fillMaxWidth()
|
||||
|
||||
val zapEvents = remember { multiSetCard.zapEvents }
|
||||
val boostEvents = remember { multiSetCard.boostEvents }
|
||||
val likeEvents = remember { multiSetCard.likeEvents }
|
||||
val zapEvents by remember { derivedStateOf { multiSetCard.zapEvents } }
|
||||
val boostEvents by remember { derivedStateOf { multiSetCard.boostEvents } }
|
||||
val likeEvents by remember { derivedStateOf { multiSetCard.likeEvents } }
|
||||
|
||||
val hasZapEvents by remember { derivedStateOf { multiSetCard.zapEvents.isNotEmpty() } }
|
||||
val hasBoostEvents by remember { derivedStateOf { multiSetCard.boostEvents.isNotEmpty() } }
|
||||
val hasLikeEvents by remember { derivedStateOf { multiSetCard.likeEvents.isNotEmpty() } }
|
||||
|
||||
Column(modifier = columnModifier) {
|
||||
if (zapEvents.isNotEmpty()) {
|
||||
RenderZapGallery(zapEvents, backgroundColor, nav, account, accountViewModel)
|
||||
if (hasZapEvents) {
|
||||
RenderZapGallery(zapEvents, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
|
||||
if (boostEvents.isNotEmpty()) {
|
||||
RenderBoostGallery(boostEvents, backgroundColor, nav, account, accountViewModel)
|
||||
if (hasBoostEvents) {
|
||||
RenderBoostGallery(boostEvents, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
|
||||
if (likeEvents.isNotEmpty()) {
|
||||
RenderLikeGallery(likeEvents, backgroundColor, nav, account, accountViewModel)
|
||||
if (hasLikeEvents) {
|
||||
RenderLikeGallery(likeEvents, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
@@ -167,7 +159,6 @@ private fun RenderLikeGallery(
|
||||
likeEvents: ImmutableList<Note>,
|
||||
backgroundColor: Color,
|
||||
nav: (String) -> Unit,
|
||||
account: Account,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
@@ -190,7 +181,7 @@ private fun RenderLikeGallery(
|
||||
)
|
||||
}
|
||||
|
||||
AuthorGallery(likeEvents, backgroundColor, nav, account, accountViewModel)
|
||||
AuthorGallery(likeEvents, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +190,6 @@ private fun RenderZapGallery(
|
||||
zapEvents: ImmutableMap<Note, Note>,
|
||||
backgroundColor: Color,
|
||||
nav: (String) -> Unit,
|
||||
account: Account,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
@@ -222,7 +212,7 @@ private fun RenderZapGallery(
|
||||
)
|
||||
}
|
||||
|
||||
AuthorGalleryZaps(zapEvents, backgroundColor, nav, account, accountViewModel)
|
||||
AuthorGalleryZaps(zapEvents, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +221,6 @@ private fun RenderBoostGallery(
|
||||
boostEvents: ImmutableList<Note>,
|
||||
backgroundColor: Color,
|
||||
nav: (String) -> Unit,
|
||||
account: Account,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Row(
|
||||
@@ -251,14 +240,14 @@ private fun RenderBoostGallery(
|
||||
null,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.size(18.dp)
|
||||
.size(19.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
},
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
|
||||
AuthorGallery(boostEvents, backgroundColor, nav, account, accountViewModel)
|
||||
AuthorGallery(boostEvents, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,15 +256,14 @@ fun AuthorGalleryZaps(
|
||||
authorNotes: ImmutableMap<Note, Note>,
|
||||
backgroundColor: Color,
|
||||
nav: (String) -> Unit,
|
||||
account: Account,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val accountState = account.userProfile().live().follows.observeAsState()
|
||||
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
FlowRow() {
|
||||
authorNotes.forEach {
|
||||
AuthorPictureAndComment(it.key, it.value, backgroundColor, nav, accountState, accountViewModel)
|
||||
Box() {
|
||||
AuthorPictureAndComment(it.key, it.value, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,55 +275,45 @@ private fun AuthorPictureAndComment(
|
||||
zapEvent: Note?,
|
||||
backgroundColor: Color,
|
||||
nav: (String) -> Unit,
|
||||
accountUser: State<UserState?>,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val author = zapRequest.author ?: return
|
||||
|
||||
var content by remember { mutableStateOf<Triple<User, String?, BigDecimal?>>(Triple(author, null, null)) }
|
||||
var content by remember { mutableStateOf<Triple<User?, String?, String?>>(Triple(zapRequest.author, null, null)) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(key1 = zapRequest.idHex, key2 = zapEvent?.idHex) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
(zapRequest.event as? LnZapRequestEvent)?.let {
|
||||
val decryptedContent = accountViewModel.decryptZap(zapRequest)
|
||||
val amount = (zapEvent?.event as? LnZapEvent)?.amount
|
||||
if (decryptedContent != null) {
|
||||
val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey)
|
||||
content = Triple(newAuthor, decryptedContent.content.ifBlank { null }, amount)
|
||||
content = Triple(newAuthor, decryptedContent.content.ifBlank { null }, showAmountAxis(amount))
|
||||
} else {
|
||||
if (!zapRequest.event?.content().isNullOrBlank() || amount != null) {
|
||||
content = Triple(author, zapRequest.event?.content()?.ifBlank { null }, amount)
|
||||
content = Triple(zapRequest.author, zapRequest.event?.content()?.ifBlank { null }, showAmountAxis(amount))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val modifier = remember(content.second) {
|
||||
if (!content.second.isNullOrBlank()) {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
nav("User/${author.pubkeyHex}")
|
||||
}
|
||||
} else {
|
||||
Modifier.clickable {
|
||||
nav("User/${author.pubkeyHex}")
|
||||
content.first?.let {
|
||||
val route by remember {
|
||||
derivedStateOf {
|
||||
"User/${it.pubkeyHex}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AuthorPictureAndComment(
|
||||
author = content.first,
|
||||
comment = content.second,
|
||||
amount = showAmountAxis(content.third),
|
||||
backgroundColor = backgroundColor,
|
||||
nav = nav,
|
||||
accountUser = accountUser,
|
||||
accountViewModel = accountViewModel,
|
||||
modifier = modifier
|
||||
)
|
||||
AuthorPictureAndComment(
|
||||
author = it,
|
||||
comment = content.second,
|
||||
amount = content.third,
|
||||
route = route,
|
||||
backgroundColor = backgroundColor,
|
||||
nav = nav,
|
||||
accountViewModel = accountViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -343,12 +321,27 @@ private fun AuthorPictureAndComment(
|
||||
author: User,
|
||||
comment: String?,
|
||||
amount: String?,
|
||||
route: String,
|
||||
backgroundColor: Color,
|
||||
nav: (String) -> Unit,
|
||||
accountUser: State<UserState?>,
|
||||
accountViewModel: AccountViewModel,
|
||||
modifier: Modifier
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val authorPictureModifier = remember { Modifier }
|
||||
|
||||
val modifier = remember(comment) {
|
||||
if (comment != null) {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
nav(route)
|
||||
}
|
||||
} else {
|
||||
Modifier.clickable {
|
||||
nav(route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
@@ -356,23 +349,27 @@ private fun AuthorPictureAndComment(
|
||||
Box(modifier = remember { Modifier.size(35.dp) }, contentAlignment = Alignment.BottomCenter) {
|
||||
FastNoteAuthorPicture(
|
||||
author = author,
|
||||
userAccount = accountUser,
|
||||
size = 35.dp
|
||||
size = 35.dp,
|
||||
accountViewModel = accountViewModel,
|
||||
pictureModifier = authorPictureModifier
|
||||
)
|
||||
|
||||
amount?.let {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colors.background.copy(0.52f)),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = BitcoinOrange,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
Box(modifier = Modifier.fillMaxSize().clip(shape = CircleShape), contentAlignment = Alignment.BottomCenter) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colors.background.copy(0.62f)),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.secondaryVariant,
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.padding(bottom = 1.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -397,34 +394,47 @@ fun AuthorGallery(
|
||||
authorNotes: ImmutableList<Note>,
|
||||
backgroundColor: Color,
|
||||
nav: (String) -> Unit,
|
||||
account: Account,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val accountState = account.userProfile().live().follows.observeAsState()
|
||||
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
FlowRow() {
|
||||
authorNotes.forEach { note ->
|
||||
note.author?.let { user ->
|
||||
val modifier = remember {
|
||||
Modifier.clickable {
|
||||
nav("User/${user.pubkeyHex}")
|
||||
}
|
||||
}
|
||||
|
||||
AuthorPictureAndComment(user, null, null, backgroundColor, nav, accountState, accountViewModel, modifier)
|
||||
Box(Modifier.size(35.dp)) {
|
||||
NotePictureAndComment(note, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotePictureAndComment(
|
||||
baseNote: Note,
|
||||
backgroundColor: Color,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val author by remember(baseNote) {
|
||||
derivedStateOf {
|
||||
baseNote.author
|
||||
}
|
||||
}
|
||||
|
||||
val route by remember(baseNote) {
|
||||
derivedStateOf {
|
||||
"User/${baseNote.author?.pubkeyHex}"
|
||||
}
|
||||
}
|
||||
|
||||
author?.let { AuthorPictureAndComment(it, null, null, route, backgroundColor, nav, accountViewModel) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FastNoteAuthorPicture(
|
||||
author: User,
|
||||
userAccount: State<UserState?>,
|
||||
size: Dp,
|
||||
pictureModifier: Modifier = Modifier
|
||||
pictureModifier: Modifier = Modifier,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val userState by author.live().metadata.observeAsState()
|
||||
val profilePicture = remember(userState) {
|
||||
@@ -435,8 +445,15 @@ fun FastNoteAuthorPicture(
|
||||
author.pubkeyHex
|
||||
}
|
||||
|
||||
val showFollowingMark = remember(userAccount.value) {
|
||||
userAccount.value?.user?.isFollowingCached(author) == true || (author === userAccount.value?.user)
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val loggedInLiveFollows = remember(accountState) { accountState?.account?.userProfile()?.live()?.follows } ?: return
|
||||
|
||||
val accountFollowsState by loggedInLiveFollows.observeAsState()
|
||||
|
||||
val showFollowingMark by remember(accountFollowsState) {
|
||||
derivedStateOf {
|
||||
accountFollowsState?.user?.isFollowingCached(author) == true || (author.pubkeyHex == accountFollowsState?.user?.pubkeyHex)
|
||||
}
|
||||
}
|
||||
|
||||
UserPicture(
|
||||
|
||||
@@ -57,14 +57,11 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.ColorMatrix
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
@@ -115,7 +112,7 @@ import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.components.VideoView
|
||||
import com.vitorpamplona.amethyst.ui.components.ZoomableContent
|
||||
@@ -166,6 +163,7 @@ fun NoteCompose(
|
||||
|
||||
val noteEvent = remember(noteState) { note.event }
|
||||
val baseChannel = remember(noteState) { note.channel() }
|
||||
val isSensitive = remember(noteState) { note.event?.isSensitive() ?: false }
|
||||
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -183,7 +181,7 @@ fun NoteCompose(
|
||||
note.let {
|
||||
NoteQuickActionMenu(it, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
}
|
||||
} else if (account.isHidden(noteForReports.author!!)) {
|
||||
} else if (account.isHidden(noteForReports.author!!) || (isSensitive && account.showSensitiveContent == false)) {
|
||||
// Does nothing
|
||||
} else {
|
||||
var showHiddenNote by remember { mutableStateOf(false) }
|
||||
@@ -192,7 +190,7 @@ fun NoteCompose(
|
||||
LaunchedEffect(key1 = noteReportsState, key2 = accountState) {
|
||||
withContext(Dispatchers.IO) {
|
||||
account.userProfile().let { loggedIn ->
|
||||
val newCanPreview = note.author === loggedIn ||
|
||||
val newCanPreview = note.author?.pubkeyHex == loggedIn.pubkeyHex ||
|
||||
(note.author?.let { loggedIn.isFollowingCached(it) } ?: true) ||
|
||||
!(noteForReports.hasAnyReports())
|
||||
|
||||
@@ -435,13 +433,10 @@ private fun RenderTextEvent(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val tags = remember(note.event?.id()) { note.event?.tags() }
|
||||
val hashtags = remember(note.event?.id()) { note.event?.hashtags() ?: emptyList() }
|
||||
val eventContent = remember(note.event?.id()) { accountViewModel.decrypt(note) }
|
||||
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
|
||||
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
|
||||
val eventContent = remember(note.event) { accountViewModel.decrypt(note) }
|
||||
|
||||
if (eventContent != null) {
|
||||
val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) }
|
||||
if (makeItShort && isAuthorTheLoggedUser) {
|
||||
Text(
|
||||
text = eventContent,
|
||||
@@ -450,16 +445,27 @@ private fun RenderTextEvent(
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
} else {
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
modifier = modifier,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
|
||||
|
||||
SensitivityWarning(
|
||||
hasSensitiveContent = hasSensitiveContent,
|
||||
accountViewModel = accountViewModel
|
||||
) {
|
||||
val modifier = remember(note.event) { Modifier.fillMaxWidth() }
|
||||
val tags = remember(note.event) { note.event?.tags() }
|
||||
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
modifier = modifier,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
|
||||
val hashtags = remember(note.event) { note.event?.hashtags() ?: emptyList() }
|
||||
DisplayUncitedHashtags(hashtags, eventContent, nav)
|
||||
}
|
||||
}
|
||||
@@ -494,25 +500,31 @@ private fun RenderPoll(
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
} else {
|
||||
TranslatableRichTextViewer(
|
||||
eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
Modifier.fillMaxWidth(),
|
||||
noteEvent.tags(),
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
|
||||
SensitivityWarning(
|
||||
hasSensitiveContent = hasSensitiveContent,
|
||||
accountViewModel = accountViewModel
|
||||
) {
|
||||
TranslatableRichTextViewer(
|
||||
eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
Modifier.fillMaxWidth(),
|
||||
noteEvent.tags(),
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
|
||||
PollNote(
|
||||
note,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
|
||||
DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, nav)
|
||||
|
||||
PollNote(
|
||||
note,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
|
||||
if (!makeItShort) {
|
||||
@@ -567,13 +579,18 @@ private fun RenderPrivateMessage(
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val noteEvent = note.event as? PrivateDmEvent ?: return
|
||||
|
||||
val withMe = remember { noteEvent.with(accountViewModel.userProfile().pubkeyHex) }
|
||||
val tags = remember(note.event?.id()) { note.event?.tags() }
|
||||
val hashtags = remember(note.event?.id()) { note.event?.hashtags() ?: emptyList() }
|
||||
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
|
||||
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
|
||||
|
||||
if (withMe) {
|
||||
val eventContent = remember { accountViewModel.decrypt(note) }
|
||||
|
||||
if (eventContent != null) {
|
||||
if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
|
||||
if (makeItShort && isAuthorTheLoggedUser) {
|
||||
Text(
|
||||
text = eventContent,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
@@ -581,17 +598,23 @@ private fun RenderPrivateMessage(
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
} else {
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tags = noteEvent.tags(),
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
|
||||
SensitivityWarning(
|
||||
hasSensitiveContent = hasSensitiveContent,
|
||||
accountViewModel = accountViewModel
|
||||
) {
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
modifier = modifier,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
|
||||
DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, nav)
|
||||
DisplayUncitedHashtags(hashtags, eventContent, nav)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1885,46 +1908,11 @@ private fun VerticalRelayPanelWithFlow(
|
||||
// FlowRow Seems to be a lot faster than LazyVerticalGrid
|
||||
FlowRow() {
|
||||
relays.forEach { url ->
|
||||
RelayIconCompose(url)
|
||||
RenderRelay(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Stable
|
||||
private fun RelayIconCompose(url: String) {
|
||||
val uri = LocalUriHandler.current
|
||||
|
||||
val model = remember(url) { "https://$url/favicon.ico" }
|
||||
|
||||
val boxModifier = remember {
|
||||
Modifier
|
||||
.padding(1.dp)
|
||||
.size(15.dp)
|
||||
}
|
||||
val iconModifier = remember(url) {
|
||||
Modifier
|
||||
.width(13.dp)
|
||||
.height(13.dp)
|
||||
.clip(shape = CircleShape)
|
||||
.clickable(onClick = { uri.openUri("https://$url") })
|
||||
}
|
||||
val colorFilter = remember {
|
||||
ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0.5f) })
|
||||
}
|
||||
|
||||
Box(boxModifier) {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = model,
|
||||
robotSize = 15.dp,
|
||||
model = model,
|
||||
contentDescription = stringResource(R.string.relay_icon),
|
||||
colorFilter = colorFilter,
|
||||
modifier = iconModifier.background(MaterialTheme.colors.background)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowMoreRelaysButton(onClick: () -> Unit) {
|
||||
val boxModifier = remember {
|
||||
@@ -2026,7 +2014,7 @@ fun UserPicture(
|
||||
baseUser: User,
|
||||
baseUserAccount: User,
|
||||
size: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
modifier: Modifier = remember { Modifier },
|
||||
onClick: ((User) -> Unit)? = null,
|
||||
onLongClick: ((User) -> Unit)? = null
|
||||
) {
|
||||
@@ -2042,7 +2030,7 @@ fun UserPicture(
|
||||
}
|
||||
|
||||
val showFollowingMark = remember(accountState) {
|
||||
accountState?.user?.isFollowingCached(baseUser) == true || baseUser === accountState?.user
|
||||
accountState?.user?.isFollowingCached(baseUser) == true || baseUser.pubkeyHex == accountState?.user?.pubkeyHex
|
||||
}
|
||||
|
||||
// BaseUser is the same reference as accountState.user
|
||||
@@ -2119,7 +2107,9 @@ fun UserPicture(
|
||||
}
|
||||
|
||||
val myIconModifier = remember {
|
||||
Modifier.width(size.div(3.5f)).height(size.div(3.5f))
|
||||
Modifier
|
||||
.width(size.div(3.5f))
|
||||
.height(size.div(3.5f))
|
||||
}
|
||||
|
||||
Box(myIconBoxModifier, contentAlignment = Alignment.Center) {
|
||||
@@ -2142,7 +2132,9 @@ data class DropDownParams(
|
||||
val isFollowingAuthor: Boolean,
|
||||
val isPrivateBookmarkNote: Boolean,
|
||||
val isPublicBookmarkNote: Boolean,
|
||||
val isLoggedUser: Boolean
|
||||
val isLoggedUser: Boolean,
|
||||
val isSensitive: Boolean,
|
||||
val showSensitiveContent: Boolean?
|
||||
)
|
||||
|
||||
@Composable
|
||||
@@ -2153,20 +2145,23 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
|
||||
var reportDialogShowing by remember { mutableStateOf(false) }
|
||||
|
||||
val bookmarkState by accountViewModel.userProfile().live().bookmarks.observeAsState()
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
|
||||
var state by remember {
|
||||
mutableStateOf<DropDownParams>(
|
||||
DropDownParams(false, false, false, false)
|
||||
DropDownParams(false, false, false, false, false, null)
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = note, key2 = bookmarkState) {
|
||||
LaunchedEffect(key1 = note, key2 = bookmarkState, key3 = accountState) {
|
||||
withContext(Dispatchers.IO) {
|
||||
state = DropDownParams(
|
||||
accountViewModel.isFollowing(note.author),
|
||||
accountViewModel.isInPrivateBookmarks(note),
|
||||
accountViewModel.isInPublicBookmarks(note),
|
||||
accountViewModel.isLoggedUser(note.author)
|
||||
isFollowingAuthor = accountViewModel.isFollowing(note.author),
|
||||
isPrivateBookmarkNote = accountViewModel.isInPrivateBookmarks(note),
|
||||
isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note),
|
||||
isLoggedUser = accountViewModel.isLoggedUser(note.author),
|
||||
isSensitive = note.event?.isSensitive() ?: false,
|
||||
showSensitiveContent = accountState?.account?.showSensitiveContent
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2244,6 +2239,24 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
|
||||
Text("Block / Report")
|
||||
}
|
||||
}
|
||||
// if (state.isSensitive) {
|
||||
Divider()
|
||||
if (state.showSensitiveContent == null || state.showSensitiveContent == true) {
|
||||
DropdownMenuItem(onClick = { accountViewModel.hideSensitiveContent(); onDismiss() }) {
|
||||
Text(stringResource(R.string.content_warning_hide_all_sensitive_content))
|
||||
}
|
||||
}
|
||||
if (state.showSensitiveContent == null || state.showSensitiveContent == false) {
|
||||
DropdownMenuItem(onClick = { accountViewModel.disableContentWarnings(); onDismiss() }) {
|
||||
Text(stringResource(R.string.content_warning_show_all_sensitive_content))
|
||||
}
|
||||
}
|
||||
if (state.showSensitiveContent != null) {
|
||||
DropdownMenuItem(onClick = { accountViewModel.seeContentWarnings(); onDismiss() }) {
|
||||
Text(stringResource(R.string.content_warning_see_warnings))
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
if (reportDialogShowing) {
|
||||
|
||||
@@ -116,10 +116,10 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
|
||||
val cardShape = RoundedCornerShape(5.dp)
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var showSelectTextDialog by remember { mutableStateOf(false) }
|
||||
var showDeleteAlertDialog by remember { mutableStateOf(false) }
|
||||
var showBlockAlertDialog by remember { mutableStateOf(false) }
|
||||
var showReportDialog by remember { mutableStateOf(false) }
|
||||
var showSelectTextDialog by remember(note) { mutableStateOf(false) }
|
||||
var showDeleteAlertDialog by remember(note) { mutableStateOf(false) }
|
||||
var showBlockAlertDialog by remember(note) { mutableStateOf(false) }
|
||||
var showReportDialog by remember(note) { mutableStateOf(false) }
|
||||
|
||||
val backgroundColor = if (MaterialTheme.colors.isLight) {
|
||||
MaterialTheme.colors.primary
|
||||
|
||||
@@ -51,7 +51,9 @@ fun PollNote(
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
|
||||
val pollViewModel: PollNoteViewModel = viewModel()
|
||||
val pollViewModel: PollNoteViewModel = viewModel(
|
||||
key = baseNote.idHex
|
||||
)
|
||||
|
||||
LaunchedEffect(key1 = baseNote) {
|
||||
pollViewModel.load(account, baseNote)
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.compose.material.icons.outlined.Bolt
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -62,6 +63,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
@@ -124,6 +126,14 @@ fun ReplyReaction(
|
||||
val repliesState by baseNote.live().replies.observeAsState()
|
||||
val replies = remember(repliesState) { repliesState?.note?.replies } ?: emptySet()
|
||||
|
||||
val isWriteable = remember { accountViewModel.isWriteable() }
|
||||
|
||||
val replyCount by remember(repliesState) {
|
||||
derivedStateOf {
|
||||
showCount(replies.size)
|
||||
}
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@@ -138,7 +148,7 @@ fun ReplyReaction(
|
||||
IconButton(
|
||||
modifier = iconButtonModifier,
|
||||
onClick = {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
if (isWriteable) {
|
||||
onPress()
|
||||
} else {
|
||||
scope.launch {
|
||||
@@ -161,7 +171,7 @@ fun ReplyReaction(
|
||||
|
||||
if (showCounter) {
|
||||
Text(
|
||||
" ${showCount(replies.size)}",
|
||||
" $replyCount",
|
||||
fontSize = 14.sp,
|
||||
color = grayTint
|
||||
)
|
||||
@@ -179,11 +189,25 @@ fun BoostReaction(
|
||||
val boostsState by baseNote.live().boosts.observeAsState()
|
||||
val boostedNote = remember(boostsState) { boostsState?.note } ?: return
|
||||
|
||||
val hasBoosted = remember(boostsState) { accountViewModel.hasBoosted(baseNote) }
|
||||
val wasBoostedByLoggedIn = remember(boostsState) { boostedNote.isBoostedBy(accountViewModel.userProfile()) }
|
||||
val hasBoosted by remember(boostsState) {
|
||||
derivedStateOf {
|
||||
accountViewModel.hasBoosted(baseNote)
|
||||
}
|
||||
}
|
||||
|
||||
val wasBoostedByLoggedIn by remember(boostsState) {
|
||||
derivedStateOf {
|
||||
boostedNote.isBoostedBy(accountViewModel.userProfile())
|
||||
}
|
||||
}
|
||||
|
||||
val isWriteable = remember { accountViewModel.isWriteable() }
|
||||
|
||||
val boostCount = remember(boostsState) { showCount(boostedNote.boosts.size) }
|
||||
val boostCount by remember(boostsState) {
|
||||
derivedStateOf {
|
||||
showCount(boostedNote.boosts.size)
|
||||
}
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -258,11 +282,25 @@ fun LikeReaction(
|
||||
val reactionsState by baseNote.live().reactions.observeAsState()
|
||||
val reactedNote = remember(reactionsState) { reactionsState?.note } ?: return
|
||||
|
||||
val hasReacted = remember(reactionsState) { accountViewModel.hasReactedTo(baseNote) }
|
||||
val wasReactedByLoggedIn = remember(reactionsState) { reactedNote.isReactedBy(accountViewModel.userProfile()) }
|
||||
val hasReacted by remember(reactionsState) {
|
||||
derivedStateOf {
|
||||
accountViewModel.hasReactedTo(baseNote)
|
||||
}
|
||||
}
|
||||
|
||||
val wasReactedByLoggedIn by remember(reactionsState) {
|
||||
derivedStateOf {
|
||||
reactedNote.isReactedBy(accountViewModel.userProfile())
|
||||
}
|
||||
}
|
||||
|
||||
val isWriteable = remember { accountViewModel.isWriteable() }
|
||||
|
||||
val reactionCount = remember(reactionsState) { showCount(reactedNote.reactions.size) }
|
||||
val reactionCount by remember(reactionsState) {
|
||||
derivedStateOf {
|
||||
showCount(reactedNote.reactions.size)
|
||||
}
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -350,13 +388,22 @@ fun ZapReaction(
|
||||
LaunchedEffect(key1 = zapsState) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (!wasZappedByLoggedInUser) {
|
||||
wasZappedByLoggedInUser = accountViewModel.calculateIfNoteWasZappedByAccount(zappedNote)
|
||||
val newWasZapped = accountViewModel.calculateIfNoteWasZappedByAccount(zappedNote)
|
||||
|
||||
if (wasZappedByLoggedInUser != newWasZapped) {
|
||||
wasZappedByLoggedInUser = newWasZapped
|
||||
}
|
||||
}
|
||||
|
||||
zapAmountTxt = showAmount(account.calculateZappedAmount(zappedNote))
|
||||
val newZapAmount = showAmount(account.calculateZappedAmount(zappedNote))
|
||||
if (newZapAmount != zapAmountTxt) {
|
||||
zapAmountTxt = newZapAmount
|
||||
}
|
||||
|
||||
if (wasZappedByLoggedInUser) {
|
||||
zappingProgress = 1f
|
||||
if (abs(zappingProgress - 1) < 0.001) {
|
||||
zappingProgress = 1f
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,10 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -40,9 +42,9 @@ import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||
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.RoyalBlue
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -53,7 +55,10 @@ import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Composable
|
||||
fun UserReactionsRow(model: UserReactionsViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit, onClick: () -> Unit) {
|
||||
fun UserReactionsRow(
|
||||
model: UserReactionsViewModel,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier
|
||||
@@ -92,6 +97,7 @@ fun UserReactionsRow(model: UserReactionsViewModel, accountViewModel: AccountVie
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class UserReactionsViewModel : ViewModel() {
|
||||
var user: User? = null
|
||||
|
||||
@@ -100,13 +106,12 @@ class UserReactionsViewModel : ViewModel() {
|
||||
var zaps by mutableStateOf<Map<String, BigDecimal>>(emptyMap())
|
||||
var replies by mutableStateOf<Map<String, Int>>(emptyMap())
|
||||
|
||||
var takenIntoAccount = setOf<HexKey>()
|
||||
|
||||
val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat()
|
||||
|
||||
var chartModel by mutableStateOf<ComposedChartEntryModel<ChartEntryModel>?>(null)
|
||||
var axisLabels by mutableStateOf<List<String>>(emptyList())
|
||||
|
||||
private var takenIntoAccount = setOf<HexKey>()
|
||||
private val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat()
|
||||
|
||||
fun load(baseUser: User) {
|
||||
user = baseUser
|
||||
reactions = emptyMap()
|
||||
@@ -275,17 +280,19 @@ class UserReactionsViewModel : ViewModel() {
|
||||
fun UserReplyReaction(
|
||||
replyCount: Int?
|
||||
) {
|
||||
val showCounts = remember(replyCount) { showCount(replyCount) }
|
||||
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_comment),
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = Color.Cyan
|
||||
tint = RoyalBlue
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
|
||||
Text(
|
||||
showCount(replyCount),
|
||||
showCounts,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
@@ -295,6 +302,8 @@ fun UserReplyReaction(
|
||||
fun UserBoostReaction(
|
||||
boostCount: Int?
|
||||
) {
|
||||
val showCounts = remember(boostCount) { showCount(boostCount) }
|
||||
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_retweeted),
|
||||
null,
|
||||
@@ -305,7 +314,7 @@ fun UserBoostReaction(
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
|
||||
Text(
|
||||
showCount(boostCount),
|
||||
showCounts,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
@@ -315,6 +324,8 @@ fun UserBoostReaction(
|
||||
fun UserLikeReaction(
|
||||
likeCount: Int?
|
||||
) {
|
||||
val showCounts = remember(likeCount) { showCount(likeCount) }
|
||||
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
@@ -325,7 +336,7 @@ fun UserLikeReaction(
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
|
||||
Text(
|
||||
showCount(likeCount),
|
||||
showCounts,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
@@ -335,6 +346,8 @@ fun UserLikeReaction(
|
||||
fun UserZapReaction(
|
||||
amount: BigDecimal?
|
||||
) {
|
||||
val showAmounts = remember(amount) { showAmountAxis(amount) }
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(R.string.zaps),
|
||||
@@ -345,7 +358,7 @@ fun UserZapReaction(
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(
|
||||
showAmountAxis(amount),
|
||||
showAmounts,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -92,9 +93,17 @@ class MessageSetCard(val note: Note) : Card() {
|
||||
override fun id() = note.idHex
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class CardFeedState {
|
||||
@Immutable
|
||||
object Loading : CardFeedState()
|
||||
|
||||
@Stable
|
||||
class Loaded(val feed: MutableState<ImmutableList<Card>>) : CardFeedState()
|
||||
|
||||
@Immutable
|
||||
object Empty : CardFeedState()
|
||||
|
||||
@Immutable
|
||||
class FeedError(val errorMessage: String) : CardFeedState()
|
||||
}
|
||||
|
||||
@@ -5,7 +5,12 @@ import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
@@ -48,9 +53,14 @@ fun CardFeedView(
|
||||
val refresh = { refreshing = true; viewModel.invalidateData(); refreshing = false }
|
||||
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh)
|
||||
|
||||
Box(Modifier.pullRefresh(pullRefreshState)) {
|
||||
Column() {
|
||||
Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state ->
|
||||
Box(Modifier.fillMaxSize().pullRefresh(pullRefreshState)) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Crossfade(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
targetState = feedState,
|
||||
animationSpec = tween(durationMillis = 100)
|
||||
) { state ->
|
||||
|
||||
when (state) {
|
||||
is CardFeedState.Empty -> {
|
||||
FeedEmpty {
|
||||
@@ -104,72 +114,97 @@ private fun FeedLoaded(
|
||||
|
||||
if (scrollToTop) {
|
||||
LaunchedEffect(Unit) {
|
||||
listState.scrollToItem(index = 0)
|
||||
if (listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0) {
|
||||
listState.scrollToItem(index = 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyNotificationList(listState, state, routeForLastRead, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LazyNotificationList(
|
||||
listState: LazyListState,
|
||||
state: CardFeedState.Loaded,
|
||||
routeForLastRead: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(
|
||||
top = 10.dp,
|
||||
bottom = 10.dp
|
||||
),
|
||||
modifier = remember { Modifier.fillMaxSize() },
|
||||
contentPadding = remember {
|
||||
PaddingValues(
|
||||
top = 10.dp,
|
||||
bottom = 10.dp
|
||||
)
|
||||
},
|
||||
state = listState
|
||||
) {
|
||||
itemsIndexed(state.feed.value, key = { _, item -> item.id() }) { _, item ->
|
||||
when (item) {
|
||||
is NoteCard -> NoteCompose(
|
||||
item.note,
|
||||
isBoostedNote = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
is ZapSetCard -> ZapSetCompose(
|
||||
item,
|
||||
isInnerNote = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
is ZapUserSetCard -> ZapUserSetCompose(
|
||||
item,
|
||||
isInnerNote = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
is LikeSetCard -> LikeSetCompose(
|
||||
item,
|
||||
isInnerNote = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
is BoostSetCard -> BoostSetCompose(
|
||||
item,
|
||||
isInnerNote = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
is MultiSetCard -> MultiSetCompose(
|
||||
item,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
is BadgeCard -> BadgeCompose(
|
||||
item,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
is MessageSetCard -> MessageSetCompose(
|
||||
messageSetCard = item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
Row(Modifier.fillMaxWidth().defaultMinSize(minHeight = 100.dp)) {
|
||||
when (item) {
|
||||
is NoteCard -> NoteCompose(
|
||||
item.note,
|
||||
isBoostedNote = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
|
||||
is ZapSetCard -> ZapSetCompose(
|
||||
item,
|
||||
isInnerNote = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
|
||||
is ZapUserSetCard -> ZapUserSetCompose(
|
||||
item,
|
||||
isInnerNote = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
|
||||
is LikeSetCard -> LikeSetCompose(
|
||||
item,
|
||||
isInnerNote = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
|
||||
is BoostSetCard -> BoostSetCompose(
|
||||
item,
|
||||
isInnerNote = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
|
||||
is MultiSetCard -> MultiSetCompose(
|
||||
item,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
|
||||
is BadgeCard -> BadgeCompose(
|
||||
item,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
|
||||
is MessageSetCard -> MessageSetCompose(
|
||||
messageSetCard = item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -33,6 +34,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@Stable
|
||||
class NotificationViewModel : CardFeedViewModel(NotificationFeedFilter)
|
||||
|
||||
open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
@@ -233,7 +235,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
private val bundler = BundledUpdate(250, Dispatchers.IO) {
|
||||
private val bundler = BundledUpdate(1000, Dispatchers.IO) {
|
||||
// adds the time to perform the refresh into this delay
|
||||
// holding off new updates in case of heavy refresh routines.
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
@@ -241,7 +243,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
}
|
||||
Log.d("Time", "${this.javaClass.simpleName} Card update $elapsed")
|
||||
}
|
||||
private val bundlerInsert = BundledInsert<Set<Note>>(250, Dispatchers.IO)
|
||||
private val bundlerInsert = BundledInsert<Set<Note>>(1000, Dispatchers.IO)
|
||||
|
||||
fun invalidateData(ignoreIfDoing: Boolean = false) {
|
||||
bundler.invalidate(ignoreIfDoing)
|
||||
|
||||
@@ -119,7 +119,9 @@ private fun FeedLoaded(
|
||||
|
||||
if (scrollToTop) {
|
||||
LaunchedEffect(Unit) {
|
||||
listState.scrollToItem(index = 0)
|
||||
if (listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0) {
|
||||
listState.scrollToItem(index = 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,8 +115,8 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
}
|
||||
private val bundlerInsert = BundledInsert<Set<Note>>(250, Dispatchers.IO)
|
||||
|
||||
fun invalidateData() {
|
||||
bundler.invalidate()
|
||||
fun invalidateData(ignoreIfDoing: Boolean = false) {
|
||||
bundler.invalidate(ignoreIfDoing)
|
||||
}
|
||||
|
||||
fun invalidateInsertData(newItems: Set<Note>) {
|
||||
|
||||
@@ -60,6 +60,7 @@ 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.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.note.*
|
||||
import com.vitorpamplona.amethyst.ui.note.BadgeDisplay
|
||||
@@ -376,15 +377,22 @@ fun NoteMaster(
|
||||
!noteForReports.hasAnyReports()
|
||||
|
||||
if (eventContent != null) {
|
||||
TranslatableRichTextViewer(
|
||||
eventContent,
|
||||
canPreview,
|
||||
Modifier.fillMaxWidth(),
|
||||
note.event?.tags(),
|
||||
MaterialTheme.colors.background,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
|
||||
|
||||
SensitivityWarning(
|
||||
hasSensitiveContent = hasSensitiveContent,
|
||||
accountViewModel = accountViewModel
|
||||
) {
|
||||
TranslatableRichTextViewer(
|
||||
eventContent,
|
||||
canPreview,
|
||||
Modifier.fillMaxWidth(),
|
||||
note.event?.tags(),
|
||||
MaterialTheme.colors.background,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
|
||||
DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, nav)
|
||||
|
||||
|
||||
+13
-1
@@ -210,7 +210,7 @@ class AccountViewModel(private val account: Account) : ViewModel() {
|
||||
}
|
||||
|
||||
fun isLoggedUser(user: User?): Boolean {
|
||||
return account.userProfile() === user
|
||||
return account.userProfile().pubkeyHex == user?.pubkeyHex
|
||||
}
|
||||
|
||||
fun isFollowing(user: User?): Boolean {
|
||||
@@ -232,6 +232,18 @@ class AccountViewModel(private val account: Account) : ViewModel() {
|
||||
account.setHideBlockAlertDialog()
|
||||
}
|
||||
|
||||
fun hideSensitiveContent() {
|
||||
account.updateShowSensitiveContent(false)
|
||||
}
|
||||
|
||||
fun disableContentWarnings() {
|
||||
account.updateShowSensitiveContent(true)
|
||||
}
|
||||
|
||||
fun seeContentWarnings() {
|
||||
account.updateShowSensitiveContent(null)
|
||||
}
|
||||
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel {
|
||||
return AccountViewModel(account) as AccountViewModel
|
||||
|
||||
@@ -218,7 +218,7 @@ fun ChannelScreen(
|
||||
onPost = {
|
||||
val tagger = NewMessageTagger(channel, listOfNotNull(replyTo.value?.author), listOfNotNull(replyTo.value), channelScreenModel.message.text)
|
||||
tagger.run()
|
||||
account.sendChannelMessage(tagger.message, channel.idHex, tagger.replyTos, tagger.mentions)
|
||||
account.sendChannelMessage(tagger.message, channel.idHex, tagger.replyTos, tagger.mentions, wantsToMarkAsSensitive = false)
|
||||
channelScreenModel.message = TextFieldValue("")
|
||||
replyTo.value = null
|
||||
feedViewModel.invalidateData() // Don't wait a full second before updating
|
||||
|
||||
@@ -178,7 +178,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, nav: (St
|
||||
trailingIcon = {
|
||||
PostButton(
|
||||
onPost = {
|
||||
account.sendPrivateMessage(chatRoomScreenModel.message.text, userId, replyTo.value, null)
|
||||
account.sendPrivateMessage(chatRoomScreenModel.message.text, userId, replyTo.value, null, wantsToMarkAsSensitive = false)
|
||||
chatRoomScreenModel.message = TextFieldValue("")
|
||||
replyTo.value = null
|
||||
feedViewModel.invalidateData() // Don't wait a full second before updating
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@@ -58,9 +59,9 @@ fun HomeScreen(
|
||||
LaunchedEffect(accountViewModel, account.defaultHomeFollowList) {
|
||||
HomeNewThreadFeedFilter.account = account
|
||||
HomeConversationsFeedFilter.account = account
|
||||
NostrHomeDataSource.resetFilters()
|
||||
homeFeedViewModel.invalidateData()
|
||||
repliesFeedViewModel.invalidateData()
|
||||
NostrHomeDataSource.invalidateFilters()
|
||||
homeFeedViewModel.invalidateData(true)
|
||||
repliesFeedViewModel.invalidateData(true)
|
||||
}
|
||||
|
||||
if (wantsToAddNip47 != null) {
|
||||
@@ -73,9 +74,7 @@ fun HomeScreen(
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
HomeNewThreadFeedFilter.account = account
|
||||
HomeConversationsFeedFilter.account = account
|
||||
NostrHomeDataSource.resetFilters()
|
||||
homeFeedViewModel.invalidateData()
|
||||
repliesFeedViewModel.invalidateData()
|
||||
NostrHomeDataSource.invalidateFilters()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +84,17 @@ fun HomeScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (scrollToTop) {
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
NostrHomeDataSource.invalidateFilters()
|
||||
homeFeedViewModel.invalidateData(true)
|
||||
repliesFeedViewModel.invalidateData(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val tabs by remember(homeFeedViewModel, repliesFeedViewModel) {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
|
||||
+27
-14
@@ -13,6 +13,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
@@ -51,6 +52,9 @@ import com.vitorpamplona.amethyst.ui.screen.CardFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.RoyalBlue
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import kotlin.math.roundToInt
|
||||
@@ -64,23 +68,20 @@ fun NotificationScreen(
|
||||
scrollToTop: Boolean = false
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
|
||||
if (scrollToTop) {
|
||||
notifFeedViewModel.clear()
|
||||
}
|
||||
|
||||
LaunchedEffect(account.userProfile().pubkeyHex, account.defaultNotificationFollowList) {
|
||||
NostrAccountDataSource.resetFilters()
|
||||
LaunchedEffect(accountViewModel, account.defaultNotificationFollowList) {
|
||||
NostrAccountDataSource.invalidateFilters()
|
||||
NotificationFeedFilter.account = account
|
||||
notifFeedViewModel.invalidateData(true)
|
||||
}
|
||||
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(accountViewModel) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
NostrAccountDataSource.invalidateFilters()
|
||||
NotificationFeedFilter.account = account
|
||||
notifFeedViewModel.invalidateData(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,11 +91,23 @@ fun NotificationScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (scrollToTop) {
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
notifFeedViewModel.clear()
|
||||
notifFeedViewModel.invalidateData(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
SummaryBar(userReactionsStatsModel, accountViewModel, nav)
|
||||
SummaryBar(
|
||||
model = userReactionsStatsModel
|
||||
)
|
||||
CardFeedView(
|
||||
viewModel = notifFeedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
@@ -108,18 +121,18 @@ fun NotificationScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SummaryBar(model: UserReactionsViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
fun SummaryBar(model: UserReactionsViewModel) {
|
||||
var showChart by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
UserReactionsRow(model, accountViewModel, nav) {
|
||||
UserReactionsRow(model) {
|
||||
showChart = !showChart
|
||||
}
|
||||
|
||||
val lineChartCount =
|
||||
lineChart(
|
||||
lines = listOf(Color.Cyan, Color.Green, Color.Red).map { lineChartColor ->
|
||||
lines = listOf(RoyalBlue, Color.Green, Color.Red).map { lineChartColor ->
|
||||
LineChart.LineSpec(
|
||||
lineColor = lineChartColor.toArgb(),
|
||||
lineBackgroundShader = DynamicShaders.fromBrush(
|
||||
@@ -153,8 +166,8 @@ fun SummaryBar(model: UserReactionsViewModel, accountViewModel: AccountViewModel
|
||||
targetVerticalAxisPosition = AxisPosition.Vertical.End
|
||||
)
|
||||
|
||||
model.chartModel?.let {
|
||||
if (showChart) {
|
||||
if (showChart) {
|
||||
model.chartModel?.let {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 10.dp, horizontal = 20.dp)
|
||||
|
||||
@@ -7,7 +7,6 @@ import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -47,12 +46,8 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.ColorMatrix
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -73,7 +68,6 @@ import com.vitorpamplona.amethyst.ui.actions.NewMediaModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewMediaView
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.dal.VideoFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.FileHeaderDisplay
|
||||
@@ -82,6 +76,7 @@ import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteDropDownMenu
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.RenderRelay
|
||||
import com.vitorpamplona.amethyst.ui.note.ViewCountReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
|
||||
@@ -91,6 +86,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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -123,7 +119,6 @@ fun VideoScreen(
|
||||
VideoFeedFilter.account = account
|
||||
NostrVideoDataSource.account = account
|
||||
NostrVideoDataSource.start()
|
||||
videoFeedView.invalidateData()
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||
println("Video Stop")
|
||||
@@ -137,6 +132,16 @@ fun VideoScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (scrollToTop) {
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
NostrVideoDataSource.resetFilters()
|
||||
videoFeedView.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
@@ -307,35 +312,13 @@ private fun RenderVideoOrPictureNote(
|
||||
@Composable
|
||||
private fun RelayBadges(baseNote: Note) {
|
||||
val noteRelaysState by baseNote.live().relays.observeAsState()
|
||||
val noteRelays = noteRelaysState?.note?.relays ?: emptySet()
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
val relaysToDisplay = noteRelays
|
||||
|
||||
val uri = LocalUriHandler.current
|
||||
val noteRelays = remember(noteRelaysState) {
|
||||
noteRelaysState?.note?.relays ?: emptySet()
|
||||
}
|
||||
|
||||
FlowRow() {
|
||||
relaysToDisplay.forEach {
|
||||
val url = it.removePrefix("wss://").removePrefix("ws://")
|
||||
Box(
|
||||
Modifier
|
||||
.size(15.dp)
|
||||
.padding(1.dp)
|
||||
) {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = "https://$url/favicon.ico",
|
||||
robotSize = 15.dp,
|
||||
model = "https://$url/favicon.ico",
|
||||
contentDescription = stringResource(id = R.string.relay_icon),
|
||||
colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }),
|
||||
modifier = Modifier
|
||||
.fillMaxSize(1f)
|
||||
.clip(shape = CircleShape)
|
||||
.background(MaterialTheme.colors.background)
|
||||
.clickable(onClick = { uri.openUri("https://$url") })
|
||||
)
|
||||
}
|
||||
noteRelays.forEach { dirtyUrl ->
|
||||
RenderRelay(dirtyUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.vitorpamplona.amethyst.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.ColorMatrix
|
||||
|
||||
val Purple200 = Color(0xFFBB86FC)
|
||||
val Purple500 = Color(0xFF6200EE)
|
||||
val Purple700 = Color(0xFF3700B3)
|
||||
val Teal200 = Color(0xFF03DAC5)
|
||||
val BitcoinOrange = Color(0xFFF7931A)
|
||||
val RoyalBlue = Color(0xFF4169E1)
|
||||
|
||||
val Following = Color(0xFF03DAC5)
|
||||
val Nip05 = Color(0xFF01BAFF)
|
||||
@@ -14,3 +17,5 @@ val FollowsFollow = Color.Yellow
|
||||
val NIP05Verified = Color.Blue
|
||||
|
||||
val WarningColor = Color(0xFFC62828)
|
||||
|
||||
val RelayIconFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0.5f) })
|
||||
|
||||
@@ -7,19 +7,22 @@ import androidx.compose.material.darkColors
|
||||
import androidx.compose.material.lightColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
|
||||
private val DarkColorPalette = darkColors(
|
||||
primary = Purple200,
|
||||
primaryVariant = Purple700,
|
||||
secondary = Teal200
|
||||
secondary = Teal200,
|
||||
secondaryVariant = Color(0xFFF7931A)
|
||||
)
|
||||
|
||||
private val LightColorPalette = lightColors(
|
||||
primary = Purple500,
|
||||
primaryVariant = Purple700,
|
||||
secondary = Teal200
|
||||
secondary = Teal200,
|
||||
secondaryVariant = Color(0xFFB66605)
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color.White,
|
||||
|
||||
@@ -385,4 +385,10 @@
|
||||
<string name="channel_list_join_channel">Csatlakozás</string>
|
||||
|
||||
<string name="today">Ma</string>
|
||||
|
||||
<string name="content_warning">Tartalomra vonatkozó figyelmeztetés</string>
|
||||
<string name="content_warning_explanation">Ez a bejegyzés érzékeny tartalmat tartalmaz, amelyet egyesek sértőnek vagy zavarónak találhatnak</string>
|
||||
<string name="content_warning_hide_all_sensitive_content">Az érzékeny tartalmat mindig rejtse el</string>
|
||||
<string name="content_warning_show_all_sensitive_content">Az érzékeny tartalmat mindig jelenítse meg</string>
|
||||
<string name="content_warning_see_warnings">A tartalomra vonatkozó figyelmeztetéseket mindig jelenítse meg</string>
|
||||
</resources>
|
||||
|
||||
@@ -404,4 +404,10 @@
|
||||
<string name="channel_list_join_channel">Join</string>
|
||||
|
||||
<string name="today">Today</string>
|
||||
|
||||
<string name="content_warning">Content warning</string>
|
||||
<string name="content_warning_explanation">This post contains sensitive content which some people may find offensive or disturbing</string>
|
||||
<string name="content_warning_hide_all_sensitive_content">Always hide sensitive content</string>
|
||||
<string name="content_warning_show_all_sensitive_content">Always show sensitive content</string>
|
||||
<string name="content_warning_see_warnings">Always show content warnings</string>
|
||||
</resources>
|
||||
|
||||
+4
-4
@@ -14,10 +14,10 @@ buildscript {
|
||||
}
|
||||
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id 'com.android.application' version '8.0.1' apply false
|
||||
id 'com.android.library' version '8.0.1' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
|
||||
id 'org.jetbrains.kotlin.jvm' version '1.8.10' apply false
|
||||
id 'com.android.application' version '8.0.2' apply false
|
||||
id 'com.android.library' version '8.0.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.8.21' apply false
|
||||
id 'org.jetbrains.kotlin.jvm' version '1.8.21' apply false
|
||||
}
|
||||
|
||||
task installGitHook(type: Copy) {
|
||||
|
||||
Reference in New Issue
Block a user