Compare commits

...

79 Commits

Author SHA1 Message Date
Vitor Pamplona
605d83e530 v0.63.0 2023-06-29 12:32:45 -04:00
Vitor Pamplona
5f710b7304 Adds #tunestr icon 2023-06-29 12:29:08 -04:00
Vitor Pamplona
1c6593fb17 Adds alt-text to screenshots 2023-06-29 12:15:03 -04:00
Vitor Pamplona
33fe34d3b3 updates screenshot titles 2023-06-29 12:13:56 -04:00
Vitor Pamplona
e257fec92c Breaking screenshots in two lines 2023-06-29 12:10:26 -04:00
Vitor Pamplona
6a70a6c72f Showing screenshots in the homepage 2023-06-29 12:09:05 -04:00
Vitor Pamplona
270d6b411a Updating Amethyst Logo to Zitron's 2023-06-29 12:00:20 -04:00
Vitor Pamplona
803b1e861e Merge pull request #480 from greenart7c3/main
added followed tags in profile screen
2023-06-29 10:03:33 -04:00
Vitor Pamplona
7300dc3d78 No need to point to the release section after the GitHub badge has been added. 2023-06-29 09:52:24 -04:00
Vitor Pamplona
405d64402b decreases height 2023-06-29 09:42:55 -04:00
Vitor Pamplona
fe1bff1074 removes space between badges 2023-06-29 09:41:44 -04:00
Vitor Pamplona
2952ca1978 Reverting change 2023-06-29 09:39:14 -04:00
Vitor Pamplona
32dcd7747b slight change in borders 2023-06-29 09:38:20 -04:00
Vitor Pamplona
eca5a1de15 Matching obtainium's and github's badge 2023-06-29 09:36:55 -04:00
Vitor Pamplona
886442ac52 trying to align obtainium's image on the readme 2023-06-29 09:35:20 -04:00
Vitor Pamplona
8521611008 Increasing borders a bit 2023-06-29 09:32:38 -04:00
Vitor Pamplona
df647e4047 Fixing borders 2023-06-29 09:31:44 -04:00
Vitor Pamplona
af26026d25 adds borders 2023-06-29 09:29:55 -04:00
Vitor Pamplona
43be55f3d6 reduces the size of the badge 2023-06-29 09:26:32 -04:00
Vitor Pamplona
9b3cb92cb3 Adds obtainium and APK to the links 2023-06-29 09:25:35 -04:00
Vitor Pamplona
f2b0ce5fd0 adds obtainium image 2023-06-29 09:24:42 -04:00
Vitor Pamplona
787ca98daf v0.62.8 2023-06-28 19:06:05 -04:00
Vitor Pamplona
71517dc454 Formatting 2023-06-28 18:54:42 -04:00
Vitor Pamplona
949301d58f Mocking Looper to use the checkMainThread in tests 2023-06-28 18:54:33 -04:00
Vitor Pamplona
cf8ac4faae Merge remote-tracking branch 'origin/HEAD'
# Conflicts:
#	app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
2023-06-28 18:32:59 -04:00
Vitor Pamplona
ad3aa124fc Performance improvements additional MainThread checks. 2023-06-28 18:32:10 -04:00
Vitor Pamplona
2326aa9f7c Runs the translation update in the main thread 2023-06-28 14:52:01 -04:00
Vitor Pamplona
acfd687d74 Avoids blinking the nip05 address 2023-06-28 14:51:34 -04:00
Vitor Pamplona
9b9183395d BugFix: Ignoring case when comparing original and translated versions. 2023-06-28 14:51:02 -04:00
greenart7c3
45b4902b2b Merge branch 'vitorpamplona:main' into main 2023-06-28 15:40:36 -03:00
greenart7c3
6056d60d32 added followed tags in profile screen 2023-06-28 15:37:24 -03:00
Vitor Pamplona
61af248c09 Merge pull request #479 from ZsZolee/main
Additional Hungarian translation
2023-06-28 11:17:16 -04:00
Vitor Pamplona
5a580db1b8 Merge pull request #478 from greenart7c3/main
fix rendering url of highlight note
2023-06-28 11:17:03 -04:00
Vitor Pamplona
a72ae49e93 Merge pull request #477 from believethehype/cashuredeem
Add copy button to Cashu Rendering, better default error message
2023-06-28 11:16:08 -04:00
Vitor Pamplona
40a01219cc Moves RichText Modifiers to singleton classes. 2023-06-28 11:13:38 -04:00
Vitor Pamplona
c652390a9c Updates URLPreview in the main thread. 2023-06-28 11:09:54 -04:00
Vitor Pamplona
0916a97526 Users rememberPagerState instead of just remember 2023-06-28 11:09:19 -04:00
Vitor Pamplona
d76ef0a1ca updates firebase to the newest version 2023-06-28 11:06:56 -04:00
Zoltan
3a70ce1587 Additional Hungarian translation 2023-06-28 16:52:39 +02:00
greenart7c3
9f96e84378 fix rendering url of highlight note 2023-06-28 07:58:19 -03:00
Believethehype
985771beba Add copy button to Cashu Rendering, better default error message 2023-06-28 11:49:14 +02:00
Vitor Pamplona
3967633f77 Merge pull request #475 from davotoula/translation_updates
Updated se/de/cs translations
2023-06-27 15:05:31 -04:00
Vitor Pamplona
ee5699c2de v0.62.7 2023-06-27 13:16:28 -04:00
Vitor Pamplona
b8f4d9f0bb BugFix on zapraiser completed bar 2023-06-27 13:14:47 -04:00
Vitor Pamplona
8b1de47517 v0.62.6 2023-06-27 12:27:06 -04:00
Vitor Pamplona
c6ff514ba2 Logs rendering time of sections of the post 2023-06-27 12:26:57 -04:00
Vitor Pamplona
50f2d18b61 Moves state assignments to the main thread. 2023-06-27 12:05:11 -04:00
Vitor Pamplona
fa7ccdfc20 Reduces full screen updates to once a second. 2023-06-27 12:02:55 -04:00
David Kaspar
fe02519737 Updated se/de/cs translations 2023-06-27 13:30:37 +01:00
Vitor Pamplona
f2e9fbebea v0.62.5 2023-06-26 22:42:42 -04:00
Vitor Pamplona
52af109b4e Minor adjustments in threading 2023-06-26 20:38:06 -04:00
Vitor Pamplona
833e6bc3e1 Moves cashu processing to a state class to account for errors. 2023-06-26 20:17:35 -04:00
Vitor Pamplona
9bed6fdb9a v0.62.4 2023-06-26 18:22:07 -04:00
Vitor Pamplona
a4a867ebcd Achieves a bigger space if the hide system bars is placed outside the Dialog. 2023-06-26 18:18:04 -04:00
Vitor Pamplona
4da2f18957 Video Dialog in full screen 2023-06-26 17:44:36 -04:00
Vitor Pamplona
4141a52d51 Allows video dialog on streaming 2023-06-26 15:40:13 -04:00
Vitor Pamplona
4dbc71d577 Removes padding from the live header 2023-06-26 15:39:32 -04:00
Vitor Pamplona
3b5d792562 Extracts Cashu logic into a service class. 2023-06-26 15:27:04 -04:00
Vitor Pamplona
dd7f3225c6 Preloading channel from LocalCache 2023-06-26 14:59:18 -04:00
Vitor Pamplona
3477e7b8ba No need to remember Modifier 2023-06-26 14:55:55 -04:00
Vitor Pamplona
a3782bb459 Remove image from the live event and use only the author picture in the header. 2023-06-26 14:47:22 -04:00
Vitor Pamplona
d4003eee04 Adjusts Profile images to crop and not fit 2023-06-26 14:46:56 -04:00
Vitor Pamplona
c9bbb66284 Adjusting logout message. 2023-06-26 14:33:21 -04:00
Vitor Pamplona
1498fa9868 Merge pull request #474 from greenart7c3/main
add confirmation dialog before logout
2023-06-26 14:27:15 -04:00
Vitor Pamplona
26b493aa1d Merge pull request #473 from believethehype/cashuredeem
Melt cashu tokens in Mint directly instead of using webservice
2023-06-26 14:26:44 -04:00
Vitor Pamplona
f9d652f849 Performance Improvements 2023-06-26 14:25:46 -04:00
greenart7c3
30b0ee444f add confirmation dialog before logout 2023-06-26 14:38:44 -03:00
Believethehype
bf8341ef58 Merge remote-tracking branch 'upstream/main' into cashuredeem 2023-06-26 18:19:01 +02:00
Believethehype
f9f1b0d98e Melt cashu tokens in Mint directly instead of using webservice 2023-06-26 18:18:45 +02:00
Vitor Pamplona
743a4c9d87 Using crossfade to show live activities 2023-06-26 09:12:10 -04:00
Vitor Pamplona
275baa7497 Merge pull request #472 from believethehype/cashuredeem
Parse and Show Sat Amount from Cashu tokens
2023-06-26 08:44:19 -04:00
Vitor Pamplona
ac1fb879c3 Bugfix for not updating user picture and name after a message arrives 2023-06-26 08:39:46 -04:00
Vitor Pamplona
270ad56450 Aligns LIVE marker to the right in the feed. 2023-06-26 08:38:29 -04:00
Believethehype
d155241efc Parse and Show Sat Amount from Cashu tokens 2023-06-26 14:32:08 +02:00
Vitor Pamplona
d61927cbee Merge pull request #471 from believethehype/cashuredeem
Parse Cashu Tokens and provide link to redeem
2023-06-26 06:50:56 -04:00
Vitor Pamplona
996b68f9c6 Merge pull request #470 from Bardesss/patch-1
Update strings.xml (NL)
2023-06-26 06:49:19 -04:00
Believethehype
f6913663d1 enable autopay, change parsing 2023-06-26 11:57:31 +02:00
Believethehype
58600e13a8 parse cashu tokens 2023-06-26 11:04:31 +02:00
Bardesss
54e15ad3cd Update strings.xml (NL)
Updated translation
2023-06-26 08:52:35 +00:00
116 changed files with 5916 additions and 1158 deletions

View File

@@ -4,6 +4,11 @@
Amethyst brings the best social network to your Android phone. Just insert your Nostr private key and start posting.
[<img src="./docs/design/obtainium.png"
alt="Get it on Obtaininum"
height="80">](https://github.com/ImranR98/Obtainium)
[<img src="https://github.com/machiav3lli/oandbackupx/raw/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png" alt="Get it on GitHub"
height="80">](https://github.com/vitorpamplona/amethyst/releases)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/com.vitorpamplona.amethyst/)
@@ -11,8 +16,6 @@ Amethyst brings the best social network to your Android phone. Just insert your
alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst)
Or get the latest APK from the [Releases Section](https://github.com/vitorpamplona/amethyst/releases/latest).
# Current Features
- [x] Event Builders / WebSocket Subscriptions (NIP-01)
@@ -191,6 +194,12 @@ Information that you share is publicly visible to anyone reading from relays tha
## Information Permanence ##
Information shared on nostr should be assumed permanent for privacy purposes. There is no way to guarantee deleting or editing any content once posted.
# Screenshots
| Home Feed | Messages | Live Streams | Notifications |
|-------------------------------------------|----------------------------------------------|-------------------------------------------------|--------------------------------------------------------|
| ![Home Feed](./docs/screenshots/home.png) | ![Messages](./docs/screenshots/messages.png) | ![Live Streams](./docs/screenshots/replies.png) | ![Notifications](./docs/screenshots/notifications.png) |
# Contributing
[Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) are very welcome.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -13,8 +13,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 33
versionCode 224
versionName "0.62.3"
versionCode 230
versionName "0.63.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -181,7 +181,7 @@ dependencies {
playImplementation 'com.google.mlkit:translate:17.0.1'
// PushNotifications
playImplementation platform('com.google.firebase:firebase-bom:32.1.0')
playImplementation platform('com.google.firebase:firebase-bom:32.1.1')
playImplementation 'com.google.firebase:firebase-messaging-ktx'
// Charts

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1134,7 +1134,7 @@ class Account(
user.countReportAuthorsBy(followingKeySet()) < 5
}
fun isAcceptableDirect(note: Note): Boolean {
private fun isAcceptableDirect(note: Note): Boolean {
if (!warnAboutPostsWithReports) {
return note.reportsBy(userProfile()).isEmpty()
}

View File

@@ -33,7 +33,7 @@ class PublicChatChannel(idHex: String) : Channel(idHex) {
}
override fun profilePicture(): String? {
if (info.picture.isNullOrBlank()) return null
if (info.picture.isNullOrBlank()) return super.profilePicture()
return info.picture ?: super.profilePicture()
}
@@ -65,7 +65,7 @@ class LiveActivitiesChannel(val address: ATag) : Channel(address.toTag()) {
}
override fun profilePicture(): String? {
return info?.image()?.ifBlank { null } ?: super.profilePicture()
return info?.image()?.ifBlank { null }
}
override fun anyNameStartsWith(prefix: String): Boolean {

View File

@@ -1,10 +1,11 @@
package com.vitorpamplona.amethyst.model
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
fun checkForHashtagWithIcon(tag: String): HashtagIcon? {
fun checkForHashtagWithIcon(tag: String, primary: Color): HashtagIcon? {
return when (tag.lowercase()) {
"bitcoin", "btc", "timechain" -> HashtagIcon(R.drawable.ht_btc, "Bitcoin", Color.Unspecified, Modifier.padding(2.dp, 2.dp, 0.dp, 0.dp))
"nostr", "nostrich", "nostriches" -> HashtagIcon(R.drawable.ht_nostr, "Nostr", Color.Unspecified, Modifier.padding(1.dp, 2.dp, 0.dp, 0.dp))
@@ -17,10 +18,13 @@ fun checkForHashtagWithIcon(tag: String): HashtagIcon? {
"skullofsatoshi" -> HashtagIcon(R.drawable.skull, "SkullofSatoshi", Color.Unspecified, Modifier.padding(2.dp, 1.dp, 0.dp, 0.dp))
"grownostr", "gardening", "garden" -> HashtagIcon(R.drawable.grownostr, "GrowNostr", Color.Unspecified, Modifier.padding(0.dp, 1.dp, 0.dp, 1.dp))
"footstr" -> HashtagIcon(R.drawable.footstr, "Footstr", Color.Unspecified, Modifier.padding(1.dp, 1.dp, 0.dp, 0.dp))
"tunestr", "music", "nowplaying" -> HashtagIcon(R.drawable.tunestr, "Tunestr", primary, Modifier.padding(1.dp, 1.dp, 0.dp, 0.dp))
"weed", "weedstr", "420", "cannabis", "marijuana" -> HashtagIcon(R.drawable.weed, "Weed", Color.Unspecified, Modifier.padding(0.dp, 0.dp, 0.dp, 0.dp))
else -> null
}
}
@Immutable
class HashtagIcon(
val icon: Int,
val description: String,

View File

@@ -51,7 +51,8 @@ object LocalCache {
}
fun getUserIfExists(key: String): User? {
return users[key]
if (key.isEmpty()) return null
return users.get(key)
}
fun getNoteIfExists(key: String): Note? {
@@ -108,6 +109,7 @@ object LocalCache {
}
private fun isValidHexNpub(key: String): Boolean {
if (key.isBlank()) return false
if (key.contains(":")) return false
return HexValidator.isHex(key)
@@ -230,7 +232,7 @@ object LocalCache {
note.loadEvent(event, author, replyTo)
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()?.take(100)} ${formattedDateTime(event.createdAt)}")
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
// Prepares user's profile view.
author.addNote(note)
@@ -302,7 +304,7 @@ object LocalCache {
note.loadEvent(event, author, replyTo)
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()?.take(100)} ${formattedDateTime(event.createdAt)}")
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
// Prepares user's profile view.
author.addNote(note)
@@ -1273,7 +1275,7 @@ class LocalCacheLiveData {
val newEventBundles = _newEventBundles.asSharedFlow() // read-only public view
// Refreshes observers in batches.
private val bundler = BundledInsert<Note>(300, Dispatchers.IO)
private val bundler = BundledInsert<Note>(1000, Dispatchers.IO)
fun invalidateData(newNote: Note) {
bundler.invalidateList(newNote) { bundledNewNotes ->

View File

@@ -77,11 +77,20 @@ open class Note(val idHex: String) {
open fun idDisplayNote() = idNote().toShortenHex()
fun channelHex(): HexKey? {
return (event as? ChannelMessageEvent)?.channel()
?: (event as? ChannelMetadataEvent)?.channel()
?: (event as? ChannelCreateEvent)?.id
?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag()
?: (event as? LiveActivitiesEvent)?.address()?.toTag()
return if (event is ChannelMessageEvent ||
event is ChannelMetadataEvent ||
event is ChannelCreateEvent ||
event is LiveActivitiesChatMessageEvent ||
event is LiveActivitiesEvent
) {
(event as? ChannelMessageEvent)?.channel()
?: (event as? ChannelMetadataEvent)?.channel()
?: (event as? ChannelCreateEvent)?.id
?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag()
?: (event as? LiveActivitiesEvent)?.address()?.toTag()
} else {
null
}
}
open fun address(): ATag? = null

View File

@@ -12,6 +12,8 @@ import com.vitorpamplona.amethyst.service.model.MetadataEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import fr.acinq.secp256k1.Hex
@@ -237,6 +239,7 @@ class User(val pubkeyHex: String) {
info = newUserInfo
info?.latestMetadata = latestMetadata
info?.updatedMetadataAt = latestMetadata.createdAt
info?.tags = latestMetadata.tags.toImmutableListOfLists()
if (newUserInfo.lud16.isNullOrBlank() && newUserInfo.lud06?.lowercase()?.startsWith("lnurl") == true) {
try {
@@ -421,6 +424,7 @@ class UserMetadata {
var updatedMetadataAt: Long = 0
var latestMetadata: MetadataEvent? = null
var tags: ImmutableListOfLists<String>? = null
fun anyName(): String? {
return display_name ?: displayName ?: name ?: username
@@ -434,6 +438,23 @@ class UserMetadata {
fun lnAddress(): String? {
return (lud16?.trim() ?: lud06?.trim())?.ifBlank { null }
}
fun bestUsername(): String? {
return name?.ifBlank { null } ?: username?.ifBlank { null }
}
fun bestDisplayName(): String? {
return displayName?.ifBlank { null } ?: display_name?.ifBlank { null }
}
fun nip05(): String? {
return nip05?.ifBlank { null }
}
fun profilePicture(): String? {
if (picture.isNullOrBlank()) picture = null
return picture
}
}
class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {

View File

@@ -49,6 +49,8 @@ object BlurHashDecoder {
* By default it is true.
*/
fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f, useCache: Boolean = true): Bitmap? {
checkNotInMainThread()
if (blurHash == null || blurHash.length < 6) {
return null
}

View File

@@ -0,0 +1,98 @@
package com.vitorpamplona.amethyst.service
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.Base64
@Immutable
data class CashuToken(
val token: String,
val mint: String,
val totalAmount: Long,
val fees: Int,
val redeemInvoiceAmount: Long,
val proofs: JsonArray
)
class CashuProcessor {
fun parse(cashuToken: String): GenericLoadable<CashuToken> {
checkNotInMainThread()
try {
val base64token = cashuToken.replace("cashuA", "")
val cashu = JsonParser.parseString(String(Base64.getDecoder().decode(base64token)))
val token = cashu.asJsonObject.get("token").asJsonArray[0].asJsonObject
val proofs = token["proofs"].asJsonArray
val mint = token["mint"].asString
var totalAmount = 0L
for (proof in proofs) {
totalAmount += proof.asJsonObject["amount"].asLong
}
val fees = Math.max(((totalAmount * 0.02).toInt()), 2)
val redeemInvoiceAmount = totalAmount - fees
return GenericLoadable.Loaded(CashuToken(cashuToken, mint, totalAmount, fees, redeemInvoiceAmount, proofs))
} catch (e: Exception) {
return GenericLoadable.Error<CashuToken>("Could not parse this cashu token")
}
}
fun melt(token: CashuToken, lud16: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
checkNotInMainThread()
runCatching {
LightningAddressResolver().lnAddressInvoice(
lnaddress = lud16,
milliSats = token.redeemInvoiceAmount * 1000, // Make invoice and leave room for fees
message = "Reedem Cashu",
onSuccess = { invoice ->
meltInvoice(token, invoice, onSuccess, onError)
},
onProgress = {
},
onError = onError
)
}
}
private fun meltInvoice(token: CashuToken, invoice: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
try {
val client = HttpClient.getHttpClient()
val url = token.mint + "/melt" // Melt cashu tokens at Mint
val jsonObject = JsonObject()
jsonObject.add("proofs", token.proofs)
jsonObject.addProperty("pr", invoice)
val mediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = jsonObject.toString().toRequestBody(mediaType)
val request = Request.Builder()
.url(url)
.post(requestBody)
.build()
val response = client.newCall(request).execute()
val body = response.body.string()
val tree = jacksonObjectMapper().readTree(body)
val successful = tree?.get("paid")?.asText() == "true"
if (successful) {
onSuccess("Redeemed ${token.totalAmount} Sats" + " (Fees: ${token.fees} Sats)")
} else {
onError(tree?.get("detail")?.asText()?.split('.')?.getOrNull(0) ?: "Cashu: Tokens already spent.")
}
} catch (e: Exception) {
onError("Token melt failure: " + e.message)
}
}
}

View File

@@ -31,7 +31,7 @@ class Nip05Verifier() {
}
private suspend fun fetchNip05JsonSuspend(nip05: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
// checkNotInMainThread()
checkNotInMainThread()
val url = assembleUrl(nip05)
@@ -49,6 +49,8 @@ class Nip05Verifier() {
HttpClient.getHttpClient().newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
checkNotInMainThread()
response.use {
if (it.isSuccessful) {
onSuccess(it.body.string())
@@ -71,13 +73,15 @@ class Nip05Verifier() {
fun verifyNip05(nip05: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
// check fails on tests
// checkNotInMainThread()
checkNotInMainThread()
val mapper = jacksonObjectMapper()
fetchNip05Json(
nip05,
onSuccess = {
checkNotInMainThread()
// NIP05 usernames are case insensitive, but JSON properties are not
// converts the json to lowercase and then tries to access the username via a
// lowercase version of the username.

View File

@@ -55,7 +55,7 @@ open class Event(
}
override fun zapraiserAmount() = tags.firstOrNull() {
(it.size > 1 && it[0].equals("zapraiser", true))
(it.size > 1 && it[0] == "zapraiser")
}?.get(1)?.toLongOrNull()
override fun zapAddress() = tags.firstOrNull { it.size > 1 && it[0] == "zap" }?.get(1)

View File

@@ -39,6 +39,7 @@ import java.net.URLEncoder
import java.nio.charset.StandardCharsets
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@@ -65,6 +65,7 @@ import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchBarViewModel
import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
@@ -389,7 +390,7 @@ fun UserComposeForChat(
),
verticalAlignment = Alignment.CenterVertically
) {
ClickableUserPicture(baseUser, 55.dp, accountViewModel)
ClickableUserPicture(baseUser, Size55dp, accountViewModel)
Column(
modifier = Modifier

View File

@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.actions
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -29,6 +30,7 @@ import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.RelayInformation
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.components.ClickableEmail
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
import com.vitorpamplona.amethyst.ui.note.LoadUser
@@ -246,7 +248,11 @@ private fun DisplayOwnerInformation(
nav: (String) -> Unit
) {
LoadUser(baseUserHex = userHex) {
UserCompose(baseUser = it, accountViewModel = accountViewModel, showDiviser = false, nav = nav)
Crossfade(it) {
if (it != null) {
UserCompose(baseUser = it, accountViewModel = accountViewModel, showDiviser = false, nav = nav)
}
}
}
}
@@ -305,6 +311,7 @@ fun loadRelayInfo(
.enqueue(
object : Callback {
override fun onResponse(call: Call, response: Response) {
checkNotInMainThread()
response.use {
val body = it.body.string()
try {

View File

@@ -0,0 +1,197 @@
package com.vitorpamplona.amethyst.ui.components
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.*
import androidx.compose.runtime.*
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.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat.startActivity
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.CashuProcessor
import com.vitorpamplona.amethyst.service.CashuToken
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun CashuPreview(cashutoken: String, accountViewModel: AccountViewModel) {
var cachuData by remember { mutableStateOf<GenericLoadable<CashuToken>>(GenericLoadable.Loading<CashuToken>()) }
LaunchedEffect(key1 = cashutoken) {
launch(Dispatchers.IO) {
val newCachuData = CashuProcessor().parse(cashutoken)
launch(Dispatchers.Main) {
cachuData = newCachuData
}
}
}
Crossfade(targetState = cachuData) {
when (it) {
is GenericLoadable.Loaded<CashuToken> -> CashuPreview(it.loaded, accountViewModel)
is GenericLoadable.Error<CashuToken> -> Text(
text = "$cashutoken ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
else -> {}
}
}
}
@Composable
fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
val lud16 = remember(accountViewModel) {
accountViewModel.account.userProfile().info?.lud16
}
val useWebService = false
val context = LocalContext.current
val scope = rememberCoroutineScope()
val clipboardManager = LocalClipboardManager.current
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 30.dp, end = 30.dp)
.clip(shape = QuoteBorder)
.border(1.dp, MaterialTheme.colors.subtleBorder, QuoteBorder)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(30.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
Icon(
painter = painterResource(R.drawable.cashu),
null,
modifier = Modifier.size(20.dp),
tint = Color.Unspecified
)
Text(
text = stringResource(R.string.cashu),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp)
)
}
Divider()
token.totalAmount.let {
Text(
text = "$it ${stringResource(id = R.string.sats)}",
fontSize = 25.sp,
fontWeight = FontWeight.W500,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 10.dp)
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
Button(
modifier = Modifier
.padding(vertical = 10.dp).padding(horizontal = 2.dp),
onClick = {
if (lud16 != null) {
scope.launch(Dispatchers.IO) {
CashuProcessor().melt(
token,
lud16,
onSuccess = {
scope.launch {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
}
},
onError = {
scope.launch {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
}
}
)
}
} else {
scope.launch {
Toast.makeText(
context,
"No Lightning Address set",
Toast.LENGTH_SHORT
).show()
}
}
},
shape = QuoteBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
) {
Text(
stringResource(R.string.cashu_redeem),
color = Color.White,
fontSize = 18.sp
)
}
Button(
modifier = Modifier
.padding(vertical = 10.dp).padding(horizontal = 1.dp),
onClick = {
if (useWebService) {
// In case we want to use the cashu.me webservice
val url = "https://redeem.cashu.me?token=$token&lightning=$lud16&autopay=false"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(context, intent, null)
} else {
// Copying the token to clipboard for now
var orignaltoken = token.token
clipboardManager.setText(AnnotatedString("$orignaltoken"))
scope.launch {
Toast.makeText(context, "Copied token to clipboard", Toast.LENGTH_SHORT).show()
}
}
},
shape = QuoteBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
) {
Text("", color = Color.White, fontSize = 18.sp)
}
}
}
}
}

View File

@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.components
import android.content.Intent
import android.net.Uri
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
@@ -25,13 +26,16 @@ fun MayBeWithdrawal(lnurlWord: String) {
}
}
lnWithdrawal?.let {
ClickableWithdrawal(withdrawalString = it)
Crossfade(targetState = lnWithdrawal) {
if (it != null) {
ClickableWithdrawal(withdrawalString = it)
} else {
Text(
text = "$lnurlWord ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
}
?: Text(
text = "$lnurlWord ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
@Composable

View File

@@ -0,0 +1,19 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.runtime.Immutable
@Immutable
sealed class GenericLoadable<T> {
@Immutable
class Loading<T> : GenericLoadable<T>()
@Immutable
class Loaded<T>(val loaded: T) : GenericLoadable<T>()
@Immutable
class Empty<T> : GenericLoadable<T>()
@Immutable
class Error<T>(val errorMessage: String) : GenericLoadable<T>()
}

View File

@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.components
import android.content.Intent
import android.net.Uri
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -27,7 +28,7 @@ import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.launch
import java.text.NumberFormat
@Composable
@@ -35,7 +36,7 @@ fun MayBeInvoicePreview(lnbcWord: String) {
var lnInvoice by remember { mutableStateOf<Pair<String, String?>?>(null) }
LaunchedEffect(key1 = lnbcWord) {
withContext(Dispatchers.IO) {
launch(Dispatchers.IO) {
val myInvoice = LnInvoiceUtil.findInvoice(lnbcWord)
if (myInvoice != null) {
val myInvoiceAmount = try {
@@ -50,13 +51,16 @@ fun MayBeInvoicePreview(lnbcWord: String) {
}
}
lnInvoice?.let {
InvoicePreview(it.first, it.second)
Crossfade(targetState = lnInvoice) {
if (it != null) {
InvoicePreview(it.first, it.second)
} else {
Text(
text = "$lnbcWord ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
}
?: Text(
text = "$lnbcWord ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
@Composable

View File

@@ -10,6 +10,7 @@ import com.abedelazizshe.lightcompressorlibrary.VideoCompressor
import com.abedelazizshe.lightcompressorlibrary.VideoQuality
import com.abedelazizshe.lightcompressorlibrary.config.AppSpecificStorageConfiguration
import com.abedelazizshe.lightcompressorlibrary.config.Configuration
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import id.zelory.compressor.Compressor
import id.zelory.compressor.constraint.default
import java.io.File
@@ -24,6 +25,8 @@ class MediaCompressor {
onReady: (Uri, String?, Long?) -> Unit,
onError: (String) -> Unit
) {
checkNotInMainThread()
if (contentType?.startsWith("video", true) == true) {
VideoCompressor.start(
context = applicationContext, // => This is required

View File

@@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.ui.components
import android.util.Log
import android.util.LruCache
import android.util.Patterns
import androidx.compose.foundation.border
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.text.InlineTextContent
@@ -16,37 +16,32 @@ import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.markdown.MarkdownParseOptions
import com.halilibo.richtext.ui.HeadingStyle
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material.MaterialRichText
import com.halilibo.richtext.ui.resolveDefaults
import com.linkedin.urls.detection.UrlDetector
import com.linkedin.urls.detection.UrlDetectorOptions
import com.vitorpamplona.amethyst.model.HashtagIcon
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Font17SP
import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
import com.vitorpamplona.amethyst.ui.theme.markdownStyle
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.amethyst.ui.uriToRoute
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
@@ -87,38 +82,6 @@ fun isValidURL(url: String?): Boolean {
}
}
internal val DefaultHeadingStyle: HeadingStyle = { level, textStyle ->
when (level) {
0 -> textStyle.copy(
fontSize = 30.sp,
fontWeight = FontWeight.Light
)
1 -> textStyle.copy(
fontSize = 26.sp,
fontWeight = FontWeight.Light
)
2 -> textStyle.copy(
fontSize = 22.sp,
fontWeight = FontWeight.Light
)
3 -> textStyle.copy(
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
4 -> textStyle.copy(
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
5 -> textStyle.copy(
fontWeight = FontWeight.Bold
)
else -> textStyle
}
}
internal val DefaultParagraphSpacing: TextUnit = 12.sp
val richTextDefaults = RichTextStyle().resolveDefaults()
fun isMarkdown(content: String): Boolean {
return content.startsWith("> ") ||
content.startsWith("# ") ||
@@ -157,7 +120,6 @@ data class RichTextViewerState(
val customEmoji: ImmutableMap<String, String>
)
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun RenderRegular(
content: String,
@@ -260,7 +222,7 @@ private fun parseUrls(
}
enum class WordType {
IMAGE, LINK, EMOJI, INVOICE, WITHDRAW, EMAIL, PHONE, BECH, HASH_INDEX, HASHTAG, SCHEMELESS_URL, OTHER
IMAGE, LINK, EMOJI, INVOICE, WITHDRAW, CASHU, EMAIL, PHONE, BECH, HASH_INDEX, HASHTAG, SCHEMELESS_URL, OTHER
}
@Composable
@@ -284,6 +246,8 @@ private fun RenderWord(
WordType.INVOICE
} else if (word.startsWith("lnurl", true)) {
WordType.WITHDRAW
} else if (word.startsWith("cashuA", true)) {
WordType.CASHU
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
WordType.EMAIL
} else if (word.length > 6 && Patterns.PHONE.matcher(word).matches()) {
@@ -339,6 +303,7 @@ private fun RenderWordWithoutPreview(
WordType.INVOICE -> NormalWord(wordSpace)
// Don't offer to withdraw
WordType.WITHDRAW -> NormalWord(wordSpace)
WordType.CASHU -> NormalWord(wordSpace)
WordType.EMAIL -> ClickableEmail(word)
WordType.PHONE -> ClickablePhone(word)
WordType.BECH -> BechLink(word, false, backgroundColor, accountViewModel, nav)
@@ -369,6 +334,7 @@ private fun RenderWordWithPreview(
WordType.EMOJI -> RenderCustomEmoji(word, state)
WordType.INVOICE -> MayBeInvoicePreview(word)
WordType.WITHDRAW -> MayBeWithdrawal(word)
WordType.CASHU -> CashuPreview(word, accountViewModel)
WordType.EMAIL -> ClickableEmail(word)
WordType.PHONE -> ClickablePhone(word)
WordType.BECH -> BechLink(word, true, backgroundColor, accountViewModel, nav)
@@ -394,6 +360,9 @@ private fun NormalWord(word: String) {
)
}
@Immutable
data class UrlWithExtraChars(val url: String, val extraChars: String)
@Composable
private fun NoProtocolUrlRenderer(word: String) {
val wordSpace = remember(word) {
@@ -401,7 +370,7 @@ private fun NoProtocolUrlRenderer(word: String) {
}
var linkedUrl by remember(word) {
mutableStateOf<Pair<String, String>?>(null)
mutableStateOf<UrlWithExtraChars?>(null)
}
LaunchedEffect(key1 = word) {
@@ -411,19 +380,30 @@ private fun NoProtocolUrlRenderer(word: String) {
val url = matcher.group(1) // url
val additionalChars = matcher.group(4) ?: "" // additional chars
linkedUrl = Pair(url, additionalChars)
launch(Dispatchers.Main) {
linkedUrl = UrlWithExtraChars(url, additionalChars)
}
}
}
}
if (linkedUrl != null) {
ClickableUrl(linkedUrl!!.first, "https://${linkedUrl!!.first}")
Text("${linkedUrl!!.second} ")
} else {
Text(
text = wordSpace,
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
Crossfade(targetState = linkedUrl) {
if (it != null) {
RenderUrl(it)
} else {
Text(
text = wordSpace,
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
}
}
@Composable
private fun RenderUrl(it: UrlWithExtraChars) {
Row() {
ClickableUrl(it.url, "https://${it.url}")
Text("${it.extraChars} ")
}
}
@@ -438,14 +418,16 @@ fun RenderCustomEmoji(word: String, state: RichTextViewerState) {
@Composable
private fun RenderContentAsMarkdown(content: String, backgroundColor: MutableState<Color>, tags: ImmutableListOfLists<String>?, nav: (String) -> Unit) {
val uri = LocalUriHandler.current
val onClick = { link: String ->
val route = uriToRoute(link)
if (route != null) {
nav(route)
} else {
runCatching { uri.openUri(link) }
val onClick = remember {
{ link: String ->
val route = uriToRoute(link)
if (route != null) {
nav(route)
} else {
runCatching { uri.openUri(link) }
}
Unit
}
Unit
}
ProvideTextStyle(MarkdownTextStyle) {
@@ -517,7 +499,7 @@ private fun ObserveNIP19Event(
launch(Dispatchers.IO) {
if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
LocalCache.checkGetOrCreateNote(it.hex)?.let { note ->
baseNote = note
launch(Dispatchers.Main) { baseNote = note }
}
}
}
@@ -554,7 +536,7 @@ private fun ObserveNIP19User(
launch(Dispatchers.IO) {
if (it.type == Nip19.Type.USER) {
LocalCache.checkGetOrCreateUser(it.hex)?.let { user ->
baseUser = user
launch(Dispatchers.Main) { baseUser = user }
}
}
}
@@ -747,26 +729,34 @@ fun BechLink(word: String, canPreview: Boolean, backgroundColor: MutableState<Co
}
}
loadedLink = LoadedBechLink(returningNote, it)
val newLink = LoadedBechLink(returningNote, it)
launch(Dispatchers.Main) {
loadedLink = newLink
}
}
}
}
if (canPreview) {
loadedLink?.let { loadedLink ->
loadedLink.baseNote?.let {
DisplayFullNote(it, accountViewModel, backgroundColor, nav, loadedLink)
} ?: run {
ClickableRoute(loadedLink.nip19, nav)
Crossfade(targetState = loadedLink) {
if (canPreview && it?.baseNote != null) {
Row() {
DisplayFullNote(it.baseNote, accountViewModel, backgroundColor, nav, it)
}
} ?: run {
Text(text = remember { "$word " })
}
} else {
loadedLink?.let {
ClickableRoute(it.nip19, nav)
} ?: run {
Text(text = remember { "$word " })
} else if (it?.nip19 != null) {
Row() {
ClickableRoute(it.nip19, nav)
}
} else {
val text = remember {
if (word.length > 16) {
word.replaceRange(8, word.length - 8, ":")
} else {
"$word "
}
}
Text(text = text, maxLines = 1)
}
}
}
@@ -779,24 +769,10 @@ private fun DisplayFullNote(
nav: (String) -> Unit,
loadedLink: LoadedBechLink
) {
val borderColor = MaterialTheme.colors.subtleBorder
val modifier = remember {
Modifier
.padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp)
.fillMaxWidth()
.clip(shape = QuoteBorder)
.border(
1.dp,
borderColor,
QuoteBorder
)
}
NoteCompose(
baseNote = it,
accountViewModel = accountViewModel,
modifier = modifier,
modifier = MaterialTheme.colors.replyModifier,
parentBackgroundColor = backgroundColor,
isQuotedNote = true,
nav = nav
@@ -817,9 +793,11 @@ private fun DisplayFullNote(
}
}
data class HashWordWithExtra(val hashtag: String, val extras: String?)
@Composable
fun HashTag(word: String, nav: (String) -> Unit) {
var tagSuffixPair by remember { mutableStateOf<Pair<String, String?>?>(null) }
var tagSuffixPair by remember { mutableStateOf<HashWordWithExtra?>(null) }
LaunchedEffect(key1 = word) {
launch(Dispatchers.IO) {
@@ -834,66 +812,82 @@ fun HashTag(word: String, nav: (String) -> Unit) {
}
if (myTag != null) {
tagSuffixPair = Pair(myTag, mySuffix)
launch(Dispatchers.Main) {
tagSuffixPair = HashWordWithExtra(myTag, mySuffix)
}
}
}
}
tagSuffixPair?.let { tagPair ->
val hashtagIcon = remember(tagPair.first) { checkForHashtagWithIcon(tagPair.first) }
ClickableText(
text = buildAnnotatedString {
withStyle(
LocalTextStyle.current.copy(color = MaterialTheme.colors.primary).toSpanStyle()
) {
append("#${tagPair.first}")
}
},
onClick = { nav("Hashtag/${tagPair.first}") }
)
if (hashtagIcon != null) {
val myId = "inlineContent"
val emptytext = buildAnnotatedString {
withStyle(
LocalTextStyle.current.copy(color = MaterialTheme.colors.primary).toSpanStyle()
) {
append("")
appendInlineContent(myId, "[icon]")
}
Crossfade(targetState = tagSuffixPair) {
if (it != null) {
Row() {
RenderHashtag(it, nav)
}
val inlineContent = mapOf(
Pair(
myId,
InlineTextContent(
Placeholder(
width = 17.sp,
height = 17.sp,
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
)
) {
Icon(
painter = painterResource(hashtagIcon.icon),
contentDescription = hashtagIcon.description,
tint = hashtagIcon.color,
modifier = hashtagIcon.modifier
)
}
)
)
// Empty Text for Size of Icon
Text(
text = emptytext,
inlineContent = inlineContent
)
} else {
Text(text = remember { "$word " })
}
tagPair.second?.ifBlank { "" }?.let {
Text(text = "$it ")
}
} ?: Text(text = "$word ")
}
}
@Composable
private fun RenderHashtag(
tagPair: HashWordWithExtra,
nav: (String) -> Unit
) {
val primary = MaterialTheme.colors.primary
val hashtagIcon = remember(tagPair.hashtag) { checkForHashtagWithIcon(tagPair.hashtag, primary) }
ClickableText(
text = buildAnnotatedString {
withStyle(
LocalTextStyle.current.copy(color = MaterialTheme.colors.primary).toSpanStyle()
) {
append("#${tagPair.hashtag}")
}
},
onClick = { nav("Hashtag/${tagPair.hashtag}") }
)
if (hashtagIcon != null) {
val myId = "inlineContent"
val emptytext = buildAnnotatedString {
withStyle(
LocalTextStyle.current.copy(color = MaterialTheme.colors.primary).toSpanStyle()
) {
append("")
appendInlineContent(myId, "[icon]")
}
}
// Empty Text for Size of Icon
Text(
text = emptytext,
inlineContent = mapOf(
myId to InlineIcon(hashtagIcon)
)
)
}
tagPair.extras?.ifBlank { "" }?.let {
Text(text = "$it ")
}
}
@Composable
private fun InlineIcon(hashtagIcon: HashtagIcon) =
InlineTextContent(
Placeholder(
width = Font17SP,
height = Font17SP,
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
)
) {
Icon(
painter = painterResource(hashtagIcon.icon),
contentDescription = hashtagIcon.description,
tint = hashtagIcon.color,
modifier = hashtagIcon.modifier
)
}
data class LoadedTag(val user: User?, val note: Note?, val addedChars: String)
@Composable
@@ -918,11 +912,15 @@ fun TagLink(word: String, tags: ImmutableListOfLists<String>, canPreview: Boolea
if (tag.size > 1) {
if (tag[0] == "p") {
LocalCache.checkGetOrCreateUser(tag[1])?.let {
loadedTag = LoadedTag(it, null, suffix)
launch(Dispatchers.Main) {
loadedTag = LoadedTag(it, null, suffix)
}
}
} else if (tag[0] == "e" || tag[0] == "a") {
LocalCache.checkGetOrCreateNote(tag[1])?.let {
loadedTag = LoadedTag(null, it, suffix)
launch(Dispatchers.Main) {
loadedTag = LoadedTag(null, it, suffix)
}
}
}
}
@@ -931,19 +929,28 @@ fun TagLink(word: String, tags: ImmutableListOfLists<String>, canPreview: Boolea
}
}
if (loadedTag == null) {
Text(
text = remember {
"$word "
Crossfade(targetState = loadedTag) {
if (it == null) {
Text(
text = remember {
"$word "
}
)
} else if (it.user != null) {
Row() {
DisplayUserFromTag(it.user, loadedTag?.addedChars ?: "", nav)
}
} else if (it.note != null) {
Row() {
DisplayNoteFromTag(
it.note,
loadedTag?.addedChars ?: "",
canPreview,
accountViewModel,
backgroundColor,
nav
)
}
)
} else {
loadedTag?.user?.let {
DisplayUserFromTag(it, loadedTag?.addedChars ?: "", nav)
}
loadedTag?.note?.let {
DisplayNoteFromTag(it, loadedTag?.addedChars ?: "", canPreview, accountViewModel, backgroundColor, nav)
}
}
}
@@ -961,15 +968,7 @@ private fun DisplayNoteFromTag(
NoteCompose(
baseNote = baseNote,
accountViewModel = accountViewModel,
modifier = Modifier
.padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp)
.fillMaxWidth()
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.subtleBorder,
QuoteBorder
),
modifier = MaterialTheme.colors.innerPostModifier,
parentBackgroundColor = backgroundColor,
isQuotedNote = true,
nav = nav
@@ -991,25 +990,25 @@ private fun DisplayUserFromTag(
) {
val route = remember { "User/${baseUser.pubkeyHex}" }
val suffix = remember { "$addedChars " }
val hex = remember { baseUser.pubkeyDisplayHex() }
val innerUserState by baseUser.live().metadata.observeAsState()
val displayName by remember(innerUserState) {
derivedStateOf {
innerUserState?.user?.toBestDisplayName() ?: ""
val meta by baseUser.live().metadata.map {
it.user.info
}.distinctUntilChanged().observeAsState(baseUser.info)
Crossfade(targetState = meta) {
Row() {
val displayName = remember(it) {
it?.bestDisplayName() ?: hex
}
CreateClickableTextWithEmoji(
clickablePart = displayName,
suffix = suffix,
maxLines = 1,
route = route,
nav = nav,
tags = it?.tags
)
}
}
val userTags by remember(innerUserState) {
derivedStateOf {
innerUserState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists()
}
}
CreateClickableTextWithEmoji(
clickablePart = displayName,
suffix = suffix,
maxLines = 1,
route = route,
nav = nav,
tags = userTags
)
}

View File

@@ -50,7 +50,7 @@ fun RobohashAsyncImage(
@Composable
fun RobohashFallbackAsyncImage(
robot: String,
model: String,
model: String?,
contentDescription: String?,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
@@ -91,28 +91,15 @@ fun RobohashAsyncImageProxy(
colorFilter: ColorFilter? = null,
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality
) {
if (model == null) {
RobohashAsyncImage(
robot = robot,
contentDescription = contentDescription,
modifier = modifier,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
filterQuality = filterQuality
)
} else {
RobohashFallbackAsyncImage(
robot = robot,
model = model,
contentDescription = contentDescription,
modifier = modifier,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
filterQuality = filterQuality
)
}
RobohashFallbackAsyncImage(
robot = robot,
model = model,
contentDescription = contentDescription,
modifier = modifier,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
filterQuality = filterQuality
)
}

View File

@@ -25,7 +25,9 @@ fun UrlPreview(url: String, urlText: String) {
LaunchedEffect(url) {
launch(Dispatchers.IO) {
UrlCachedPreviewer.previewInfo(url) {
urlPreviewState = it
launch(Dispatchers.Main) {
urlPreviewState = it
}
}
}
}

View File

@@ -62,6 +62,8 @@ import com.vitorpamplona.amethyst.VideoCache
import com.vitorpamplona.amethyst.service.HttpClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
public var DefaultMutedSetting = mutableStateOf(true)
@@ -97,12 +99,26 @@ fun LoadThumbAndThenVideoView(videoUri: String, description: String? = null, thu
}
}
@OptIn(ExperimentalTime::class)
@Composable
fun VideoView(
videoUri: String,
description: String? = null,
thumb: VideoThumb? = null,
onDialog: ((Boolean) -> Unit)? = null
) {
val (value, elapsed) = measureTimedValue {
VideoView1(videoUri, description, thumb, onDialog)
}
Log.d("Rendering Metrics", "VideoView $elapsed $videoUri")
}
@Composable
fun VideoView1(
videoUri: String,
description: String? = null,
thumb: VideoThumb? = null,
onDialog: ((Boolean) -> Unit)? = null
) {
var exoPlayerData by remember { mutableStateOf<VideoPlayer?>(null) }
val defaultToStart by remember { mutableStateOf(DefaultMutedSetting.value) }
@@ -127,6 +143,7 @@ fun VideoView(
}
}
@OptIn(ExperimentalTime::class)
@Composable
fun VideoView(
videoUri: String,
@@ -135,6 +152,21 @@ fun VideoView(
defaultToStart: Boolean = false,
thumb: VideoThumb? = null,
onDialog: ((Boolean) -> Unit)? = null
) {
val (value, elapsed) = measureTimedValue {
VideoView1(videoUri, description, exoPlayerData, defaultToStart, thumb, onDialog)
}
Log.d("Rendering Metrics", "VideoView $elapsed $videoUri")
}
@Composable
fun VideoView1(
videoUri: String,
description: String? = null,
exoPlayerData: VideoPlayer,
defaultToStart: Boolean = false,
thumb: VideoThumb? = null,
onDialog: ((Boolean) -> Unit)? = null
) {
val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)

View File

@@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Divider
import androidx.compose.material.Icon
@@ -25,6 +24,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
@@ -33,12 +33,10 @@ fun ZapRaiserRequest(
newPostViewModel: NewPostViewModel
) {
Column(
modifier = Modifier
.fillMaxWidth()
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier
.fillMaxWidth()
modifier = Modifier.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -49,7 +47,7 @@ fun ZapRaiserRequest(
Icon(
painter = painterResource(R.drawable.lightning),
null,
modifier = Modifier.size(20.dp),
modifier = Size20Modifier,
tint = Color.Unspecified
)

View File

@@ -1,6 +1,7 @@
package com.vitorpamplona.amethyst.ui.components
import android.content.Context
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -10,10 +11,10 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -21,9 +22,11 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
@@ -36,6 +39,7 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Report
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -50,6 +54,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.Placeholder
@@ -439,13 +444,34 @@ private fun DisplayBlurHash(
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: ImmutableList<ZoomableContent> = listOf(imageUrl).toImmutableList(), onDismiss: () -> Unit) {
val view = LocalView.current
DisposableEffect(key1 = Unit) {
if (Build.VERSION.SDK_INT >= 30) {
view.windowInsetsController?.hide(
android.view.WindowInsets.Type.systemBars()
)
}
onDispose {
if (Build.VERSION.SDK_INT >= 30) {
view.windowInsetsController?.show(
android.view.WindowInsets.Type.systemBars()
)
}
}
}
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false)
properties = DialogProperties(
usePlatformDefaultWidth = false,
decorFitsSystemWindows = false
)
) {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Column() {
val pagerState: PagerState = remember { PagerState() }
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
val pagerState: PagerState = rememberPagerState()
LaunchedEffect(key1 = pagerState, key2 = imageUrl) {
val page = allImages.indexOf(imageUrl)
@@ -454,6 +480,18 @@ fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: ImmutableList<Zoom
}
}
if (allImages.size > 1) {
SlidingCarousel(
pagerState = pagerState,
itemsCount = allImages.size,
itemContent = { index ->
RenderImageOrVideo(allImages[index])
}
)
} else {
RenderImageOrVideo(imageUrl)
}
Row(
modifier = Modifier
.padding(10.dp)
@@ -470,18 +508,6 @@ fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: ImmutableList<Zoom
SaveToGallery(localFile = myContent.localFile, mimeType = myContent.mimeType)
}
}
if (allImages.size > 1) {
SlidingCarousel(
pagerState = pagerState,
itemsCount = allImages.size,
itemContent = { index ->
RenderImageOrVideo(allImages[index])
}
)
} else {
RenderImageOrVideo(imageUrl)
}
}
}
}

View File

@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.AlertDialog
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
@@ -246,8 +247,42 @@ private fun LogoutButton(
acc: AccountInfo,
accountStateViewModel: AccountStateViewModel
) {
var logoutDialog by remember { mutableStateOf(false) }
if (logoutDialog) {
AlertDialog(
title = {
Text(text = stringResource(R.string.log_out))
},
text = {
Text(text = stringResource(R.string.are_you_sure_you_want_to_log_out))
},
onDismissRequest = {
logoutDialog = false
},
confirmButton = {
TextButton(
onClick = {
logoutDialog = false
accountStateViewModel.logOff(acc.npub)
}
) {
Text(text = stringResource(R.string.log_out))
}
},
dismissButton = {
TextButton(
onClick = {
logoutDialog = false
}
) {
Text(text = stringResource(R.string.cancel))
}
}
)
}
IconButton(
onClick = { accountStateViewModel.logOff(acc.npub) }
onClick = { logoutDialog = true }
) {
Icon(
imageVector = Icons.Default.Logout,

View File

@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -53,9 +54,14 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ChatHeadlineBorders
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.Height4dpModifier
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.Size75dp
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
@@ -84,7 +90,7 @@ fun ChatroomComposeChannelOrUser(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val channelHex by remember {
val channelHex by remember(baseNote) {
derivedStateOf {
baseNote.channelHex()
}
@@ -103,15 +109,23 @@ private fun ChatroomDirectMessage(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val userRoomHex by remember {
val userRoomHex by remember(baseNote) {
derivedStateOf {
(baseNote.event as? PrivateDmEvent)?.talkingWith(accountViewModel.userProfile().pubkeyHex)
}
}
userRoomHex?.let {
LoadUser(it) { user ->
UserRoomCompose(baseNote, user, accountViewModel, nav)
LoadUser(it) { baseUser ->
Crossfade(baseUser) { user ->
if (user != null) {
UserRoomCompose(baseNote, user, accountViewModel, nav)
} else {
Box(Modifier.height(Size75dp).fillMaxWidth()) {
// Makes sure just a max amount of objects are loaded.
}
}
}
}
}
}
@@ -246,6 +260,7 @@ private fun UserRoomCompose(
accountViewModel.decrypt(note)
}
}
WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages ->
if (hasNewMessages.value != newHasNewMessages) {
hasNewMessages.value = newHasNewMessages
@@ -288,7 +303,7 @@ private fun WatchNotificationChanges(
}
@Composable
fun LoadUser(baseUserHex: String, content: @Composable (User) -> Unit) {
fun LoadUser(baseUserHex: String, content: @Composable (User?) -> Unit) {
var user by remember(baseUserHex) {
mutableStateOf<User?>(LocalCache.getUserIfExists(baseUserHex))
}
@@ -301,9 +316,7 @@ fun LoadUser(baseUserHex: String, content: @Composable (User) -> Unit) {
}
}
user?.let {
content(it)
}
content(user)
}
@Composable
@@ -348,9 +361,7 @@ fun ChannelName(
onClick: () -> Unit
) {
Column(modifier = remember { Modifier.clickable(onClick = onClick) }) {
Row(
modifier = remember { Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp) }
) {
Row(modifier = ChatHeadlineBorders) {
Column(Size55Modifier) {
channelPicture()
}
@@ -362,11 +373,13 @@ fun ChannelName(
verticalArrangement = Arrangement.SpaceAround
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = remember { Modifier.padding(bottom = 4.dp) }
verticalAlignment = Alignment.CenterVertically
) {
FirstRow(channelTitle, channelLastTime, remember { Modifier.weight(1f) })
}
Spacer(modifier = Height4dpModifier)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
@@ -377,8 +390,8 @@ fun ChannelName(
}
Divider(
modifier = remember { Modifier.padding(top = 10.dp) },
thickness = 0.25.dp
modifier = StdTopPadding,
thickness = DividerThickness
)
}
}

View File

@@ -1,7 +1,8 @@
package com.vitorpamplona.amethyst.ui.note
import android.util.Log
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
@@ -10,7 +11,6 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -38,34 +38,49 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserMetadata
import com.vitorpamplona.amethyst.service.firstFullChar
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.CombinedZap
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifier
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifierSmaller
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size18dp
import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifier
import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifierWithPadding
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import com.vitorpamplona.amethyst.ui.theme.overPictureBackground
import com.vitorpamplona.amethyst.ui.theme.profile35dpModifier
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class)
@Composable
fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val baseNote = remember { multiSetCard.note }
@@ -90,7 +105,9 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
}
if (backgroundColor.value != newBackgroundColor) {
backgroundColor.value = newBackgroundColor
launch(Dispatchers.Main) {
backgroundColor.value = newBackgroundColor
}
}
}
}
@@ -117,32 +134,39 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
}
Column(modifier = columnModifier) {
Galeries(multiSetCard, backgroundColor, nav, accountViewModel)
val (value, elapsed) = measureTimedValue {
Galeries(multiSetCard, backgroundColor, accountViewModel, nav)
}
Log.d("Rendering Metrics", "All Galeries: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed - ")
Row(remember { Modifier.fillMaxWidth() }) {
Spacer(modifier = remember { Modifier.width(65.dp) })
Spacer(modifier = WidthAuthorPictureModifierWithPadding)
NoteCompose(
baseNote = baseNote,
routeForLastRead = null,
modifier = remember { Modifier.padding(top = 5.dp) },
isBoostedNote = true,
parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav
)
val (value, elapsed) = measureTimedValue {
NoteCompose(
baseNote = baseNote,
routeForLastRead = null,
modifier = remember { Modifier.padding(top = 5.dp) },
isBoostedNote = true,
parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav
)
}
Log.d("Rendering Metrics", "Complete: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
NoteDropDownMenu(baseNote, popupExpanded, { popupExpanded = false }, accountViewModel)
}
}
}
@OptIn(ExperimentalTime::class)
@Composable
private fun Galeries(
multiSetCard: MultiSetCard,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val zapEvents by remember { derivedStateOf { multiSetCard.zapEvents } }
val boostEvents by remember { derivedStateOf { multiSetCard.boostEvents } }
@@ -153,17 +177,26 @@ private fun Galeries(
val hasLikeEvents by remember { derivedStateOf { multiSetCard.likeEvents.isNotEmpty() } }
if (hasZapEvents) {
RenderZapGallery(zapEvents, backgroundColor, nav, accountViewModel)
val (value, elapsed) = measureTimedValue {
RenderZapGallery(zapEvents, backgroundColor, nav, accountViewModel)
}
Log.d("Rendering Metrics", "Galeries Zaps: ${multiSetCard.note.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
}
if (hasBoostEvents) {
RenderBoostGallery(boostEvents, backgroundColor, nav, accountViewModel)
val (value, elapsed) = measureTimedValue {
RenderBoostGallery(boostEvents, nav, accountViewModel)
}
Log.d("Rendering Metrics", "Galeries Repost: ${multiSetCard.note.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
}
if (hasLikeEvents) {
likeEvents.forEach {
RenderLikeGallery(it.key, it.value, backgroundColor, nav, accountViewModel)
val (value, elapsed) = measureTimedValue {
likeEvents.forEach {
RenderLikeGallery(it.key, it.value, nav, accountViewModel)
}
}
Log.d("Rendering Metrics", "Galeries Like: ${multiSetCard.note.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
}
}
@@ -171,39 +204,23 @@ private fun Galeries(
fun RenderLikeGallery(
reactionType: String,
likeEvents: ImmutableList<Note>,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
val isNotEmpty = remember(likeEvents) {
likeEvents.isNotEmpty()
}
val shortReaction by remember {
derivedStateOf {
reactionType.firstFullChar()
}
}
if (isNotEmpty) {
Row(remember { Modifier.fillMaxWidth() }) {
if (likeEvents.isNotEmpty()) {
Row(Modifier.fillMaxWidth()) {
Box(
modifier = remember {
Modifier
.width(55.dp)
.padding(end = 5.dp)
}
modifier = NotificationIconModifier
) {
val modifier = remember {
Modifier
.align(Alignment.TopEnd)
Modifier.align(Alignment.TopEnd)
}
when (shortReaction) {
when (val shortReaction = reactionType.firstFullChar()) {
"+" -> Icon(
painter = painterResource(R.drawable.ic_liked),
null,
modifier = remember { modifier.size(18.dp) },
modifier = modifier.size(Size18dp),
tint = Color.Unspecified
)
"-" -> Text(text = "\uD83D\uDC4E", modifier = modifier)
@@ -211,7 +228,7 @@ fun RenderLikeGallery(
}
}
AuthorGallery(likeEvents, backgroundColor, nav, accountViewModel)
AuthorGallery(likeEvents, nav, accountViewModel)
}
}
}
@@ -233,21 +250,17 @@ fun RenderZapGallery(
@Composable
private fun ZapIcon() {
Box(
modifier = remember {
Modifier
.width(55.dp)
.padding(0.dp)
}
modifier = WidthAuthorPictureModifier
) {
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = "Zaps",
tint = BitcoinOrange,
modifier = remember {
Modifier
.size(25.dp)
.align(Alignment.TopEnd)
}
modifier =
Modifier
.size(Size25dp)
.align(Alignment.TopEnd)
)
}
}
@@ -255,21 +268,14 @@ private fun ZapIcon() {
@Composable
fun RenderBoostGallery(
boostEvents: ImmutableList<Note>,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
Row(
modifier = remember {
Modifier.fillMaxWidth()
}
modifier = Modifier.fillMaxWidth()
) {
Box(
modifier = remember {
Modifier
.width(55.dp)
.padding(end = 4.dp)
}
modifier = NotificationIconModifierSmaller
) {
Icon(
painter = painterResource(R.drawable.ic_retweeted),
@@ -283,7 +289,7 @@ fun RenderBoostGallery(
)
}
AuthorGallery(boostEvents, backgroundColor, nav, accountViewModel)
AuthorGallery(boostEvents, nav, accountViewModel)
}
}
@@ -295,15 +301,27 @@ fun AuthorGalleryZaps(
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
Column(modifier = remember { Modifier.padding(start = 10.dp) }) {
Column(modifier = StdStartPadding) {
FlowRow() {
authorNotes.forEach {
AuthorPictureAndComment(it.request, it.response, backgroundColor, nav, accountViewModel)
ParseAuthorCommentAndAmount(it, backgroundColor, nav, accountViewModel)
}
}
}
}
@Composable
private fun ParseAuthorCommentAndAmount(
zap: CombinedZap,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
ParseAuthorCommentAndAmount(zap.request, zap.response, accountViewModel) { state ->
RenderState(state, backgroundColor, accountViewModel, nav)
}
}
@Immutable
data class ZapAmountCommentNotification(
val user: User?,
@@ -312,12 +330,11 @@ data class ZapAmountCommentNotification(
)
@Composable
private fun AuthorPictureAndComment(
private fun ParseAuthorCommentAndAmount(
zapRequest: Note,
zapEvent: Note?,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
accountViewModel: AccountViewModel,
onReady: @Composable (MutableState<ZapAmountCommentNotification>) -> Unit
) {
val content = remember {
mutableStateOf(
@@ -330,29 +347,44 @@ private fun AuthorPictureAndComment(
}
LaunchedEffect(key1 = zapRequest.idHex, key2 = zapEvent?.idHex) {
launch(Dispatchers.Default) {
launch(Dispatchers.IO) {
(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.value = ZapAmountCommentNotification(newAuthor, decryptedContent.content.ifBlank { null }, showAmountAxis(amount))
val newState = ZapAmountCommentNotification(newAuthor, decryptedContent.content.ifBlank { null }, showAmountAxis(amount))
launch(Dispatchers.Main) { content.value = newState }
} else {
if (!zapRequest.event?.content().isNullOrBlank() || amount != null) {
content.value = ZapAmountCommentNotification(zapRequest.author, zapRequest.event?.content()?.ifBlank { null }, showAmountAxis(amount))
val newState = ZapAmountCommentNotification(zapRequest.author, zapRequest.event?.content()?.ifBlank { null }, showAmountAxis(amount))
launch(Dispatchers.Main) { content.value = newState }
}
}
}
}
}
onReady(content)
}
@Composable
private fun RenderState(
content: MutableState<ZapAmountCommentNotification>,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
Row(
modifier = Modifier.clickable {
nav("User/${content.value.user?.pubkeyHex}")
content.value.user?.let {
nav(routeFor(it))
}
},
verticalAlignment = Alignment.CenterVertically
) {
AuthorPictureAndComment(
DisplayAuthorCommentAndAmount(
authorComment = content,
backgroundColor = backgroundColor,
nav = nav,
@@ -362,91 +394,88 @@ private fun AuthorPictureAndComment(
}
val amountBoxModifier = Modifier
.fillMaxSize()
.size(Size35dp)
.clip(shape = CircleShape)
val textBoxModifier = Modifier.padding(start = 5.dp).fillMaxWidth()
val simpleModifier = Modifier
val sizedModifier = Modifier.size(Size35dp)
val textBoxModifier = Modifier
.padding(start = 5.dp)
.fillMaxWidth()
val bottomPadding1dp = Modifier.padding(bottom = 1.dp)
val commentTextSize = 12.sp
@Composable
private fun AuthorPictureAndComment(
private fun DisplayAuthorCommentAndAmount(
authorComment: MutableState<ZapAmountCommentNotification>,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
Box(modifier = sizedModifier, contentAlignment = Alignment.BottomCenter) {
DisplayPicture(authorComment, accountViewModel)
DisplayAmount(authorComment)
Box(modifier = Size35Modifier, contentAlignment = Alignment.BottomCenter) {
CrossfadeToDisplayPicture(authorComment, accountViewModel)
CrossfadeToDisplayAmount(authorComment)
}
DisplayComment(authorComment, backgroundColor, nav, accountViewModel)
CrossfadeToDisplayComment(authorComment, backgroundColor, nav, accountViewModel)
}
@Composable
fun DisplayPicture(authorComment: MutableState<ZapAmountCommentNotification>, accountViewModel: AccountViewModel) {
authorComment.value.user?.let {
FastNoteAuthorPicture(
author = it,
size = Size35dp,
accountViewModel = accountViewModel,
pictureModifier = simpleModifier
)
fun CrossfadeToDisplayPicture(authorComment: MutableState<ZapAmountCommentNotification>, accountViewModel: AccountViewModel) {
Crossfade(authorComment.value) {
WatchUserMetadataAndFollowsAndRenderUserProfilePictureOrDefaultAuthor(it.user, accountViewModel)
}
}
@Composable
fun DisplayAmount(authorComment: MutableState<ZapAmountCommentNotification>) {
authorComment.value.amount?.let {
Box(
modifier = amountBoxModifier,
contentAlignment = Alignment.BottomCenter
) {
val backgroundColor = MaterialTheme.colors.overPictureBackground
fun CrossfadeToDisplayAmount(authorComment: MutableState<ZapAmountCommentNotification>) {
Crossfade(authorComment.value, modifier = amountBoxModifier) {
it.amount?.let {
Box(
modifier = remember {
Modifier
.fillMaxWidth()
.drawBehind { drawRect(backgroundColor) }
},
modifier = amountBoxModifier,
contentAlignment = Alignment.BottomCenter
) {
Text(
text = it,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.secondaryVariant,
fontSize = commentTextSize,
modifier = bottomPadding1dp
)
val backgroundColor = MaterialTheme.colors.overPictureBackground
Box(
modifier = remember {
Modifier
.width(Size35dp)
.drawBehind { drawRect(backgroundColor) }
},
contentAlignment = Alignment.BottomCenter
) {
Text(
text = it,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.secondaryVariant,
fontSize = commentTextSize,
modifier = bottomPadding1dp
)
}
}
}
}
}
@Composable
fun DisplayComment(
fun CrossfadeToDisplayComment(
authorComment: MutableState<ZapAmountCommentNotification>,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
authorComment.value.comment?.let {
TranslatableRichTextViewer(
content = it,
canPreview = true,
tags = remember { ImmutableListOfLists() },
modifier = textBoxModifier,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav
)
Crossfade(authorComment.value) {
it.comment?.let {
TranslatableRichTextViewer(
content = it,
canPreview = true,
tags = remember { ImmutableListOfLists() },
modifier = textBoxModifier,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav
)
}
}
}
@@ -454,14 +483,13 @@ fun DisplayComment(
@Composable
fun AuthorGallery(
authorNotes: ImmutableList<Note>,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
Column(modifier = remember { Modifier.padding(start = 10.dp) }) {
Column(modifier = StdStartPadding) {
FlowRow() {
authorNotes.forEach { note ->
BoxedAuthor(note, backgroundColor, nav, accountViewModel)
BoxedAuthor(note, nav, accountViewModel)
}
}
}
@@ -470,100 +498,78 @@ fun AuthorGallery(
@Composable
private fun BoxedAuthor(
note: Note,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
Box(sizedModifier) {
NotePictureAndComment(note, backgroundColor, nav, accountViewModel)
}
}
@Composable
private fun NotePictureAndComment(
baseNote: Note,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
val author = remember(baseNote) {
mutableStateOf(
ZapAmountCommentNotification(
user = baseNote.author,
comment = null,
amount = null
)
)
}
val modifier = remember(baseNote) {
Modifier.clickable {
nav("User/${baseNote.author?.pubkeyHex}")
}
}
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
AuthorPictureAndComment(authorComment = author, backgroundColor, nav, accountViewModel)
}
}
@Composable
fun FastNoteAuthorPicture(
author: User,
size: Dp,
pictureModifier: Modifier = Modifier,
accountViewModel: AccountViewModel
) {
val myBoxModifier = remember {
Modifier.size(size)
}
Box(myBoxModifier, contentAlignment = Alignment.TopEnd) {
WatchMetadataAndRenderPictureWithFollwingMark(author, size, pictureModifier, accountViewModel)
}
}
@Composable
private fun WatchMetadataAndRenderPictureWithFollwingMark(
author: User,
size: Dp,
pictureModifier: Modifier,
accountViewModel: AccountViewModel
) {
var profilePicture by remember {
mutableStateOf(author.info?.picture)
}
val authorPubKey = remember {
author.pubkeyHex
}
WatchUserMetadata(author) {
if (it.picture != profilePicture) {
profilePicture = it.picture
}
}
PictureAndFollowingMark(
userHex = authorPubKey,
userPicture = profilePicture,
size = size,
modifier = pictureModifier,
accountViewModel = accountViewModel
)
}
@Composable
fun WatchUserMetadata(userBase: User, onMetadataChanges: (UserMetadata) -> Unit) {
val userState by userBase.live().metadata.observeAsState()
LaunchedEffect(key1 = userState) {
launch(Dispatchers.Default) {
userState?.user?.info?.let {
onMetadataChanges(it)
Box(modifier = Size35Modifier.clickable(onClick = { nav(authorRouteFor(note)) })) {
WatchNoteAuthor(note) { targetAuthor ->
Crossfade(targetState = targetAuthor, modifier = Size35Modifier) { author ->
WatchUserMetadataAndFollowsAndRenderUserProfilePictureOrDefaultAuthor(author, accountViewModel)
}
}
}
}
@Composable
fun WatchUserMetadataAndFollowsAndRenderUserProfilePictureOrDefaultAuthor(
author: User?,
accountViewModel: AccountViewModel
) {
if (author != null) {
WatchUserMetadataAndFollowsAndRenderUserProfilePicture(author, accountViewModel)
} else {
DisplayBlankAuthor(Size35dp)
}
}
@Composable
fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture(
author: User,
accountViewModel: AccountViewModel
) {
WatchUserMetadata(author) { baseUserPicture ->
Crossfade(targetState = baseUserPicture) { userPicture ->
RobohashAsyncImageProxy(
robot = author.pubkeyHex,
model = userPicture,
contentDescription = stringResource(id = R.string.profile_image),
modifier = MaterialTheme.colors.profile35dpModifier,
contentScale = ContentScale.Crop
)
}
}
WatchFollows(author.pubkeyHex, accountViewModel) { isFollowing ->
Crossfade(targetState = isFollowing) {
if (it) {
Box(modifier = Size35Modifier, contentAlignment = Alignment.TopEnd) {
FollowingIcon(Size10dp)
}
}
}
}
}
@Composable
private fun WatchNoteAuthor(
baseNote: Note,
onContent: @Composable (User?) -> Unit
) {
val author by baseNote.live().metadata.map {
it.note.author
}.observeAsState(baseNote.author)
onContent(author)
}
@Composable
private fun WatchUserMetadata(
author: User,
onNewMetadata: @Composable (String?) -> Unit
) {
val userProfile by author.live().metadata.map {
it.user.profilePicture()
}.distinctUntilChanged().observeAsState(author.profilePicture())
onNewMetadata(userProfile)
}

View File

@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
@@ -28,6 +29,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
@@ -109,21 +111,16 @@ fun ObserveDisplayNip05Status(baseNote: Note, columnModifier: Modifier = Modifie
@Composable
fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifier) {
val userState by baseUser.live().metadata.observeAsState()
val isValidNIP05 by remember(userState) {
derivedStateOf {
userState?.user?.nip05()?.split("@")?.size == 2
}
}
val nip05 by remember(userState) {
derivedStateOf {
userState?.user?.nip05()
}
}
val nip05 by baseUser.live().metadata.map {
it.user.nip05()
}.observeAsState(baseUser.nip05())
if (isValidNIP05) {
nip05?.let {
DisplayNIP05Line(it, baseUser, columnModifier)
Crossfade(targetState = nip05, modifier = columnModifier) {
if (it != null) {
val isValid = it.split("@").size == 2
if (isValid) {
DisplayNIP05Line(it, baseUser, columnModifier)
}
}
}
}
@@ -131,9 +128,11 @@ fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifie
@Composable
private fun DisplayNIP05Line(nip05: String, baseUser: User, columnModifier: Modifier = Modifier) {
Column(modifier = columnModifier) {
Row(verticalAlignment = Alignment.CenterVertically) {
val nip05Verified = nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex)
DisplayNIP05(nip05, nip05Verified)
val nip05Verified = nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex)
Crossfade(targetState = nip05Verified) {
Row(verticalAlignment = Alignment.CenterVertically) {
DisplayNIP05(nip05, it)
}
}
}
}
@@ -150,7 +149,7 @@ private fun DisplayNIP05(
if (user != "_") {
Text(
text = AnnotatedString(user),
text = remember(nip05) { AnnotatedString(user) },
color = MaterialTheme.colors.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis
@@ -160,7 +159,7 @@ private fun DisplayNIP05(
NIP05VerifiedSymbol(nip05Verified)
ClickableText(
text = AnnotatedString(domain),
text = remember(nip05) { AnnotatedString(domain) },
onClick = { runCatching { uri.openUri("https://$domain") } },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(0.52f)),
maxLines = 1,

View File

@@ -19,10 +19,10 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.CircularProgressIndicator
@@ -51,6 +51,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.Center
import androidx.compose.ui.Alignment.Companion.CenterStart
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -65,6 +66,8 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import coil.compose.AsyncImage
@@ -83,6 +86,7 @@ import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
import com.vitorpamplona.amethyst.ui.theme.Height4dpModifier
import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp
import com.vitorpamplona.amethyst.ui.theme.NoSoTinyBorders
import com.vitorpamplona.amethyst.ui.theme.ReactionRowExpandButton
import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeight
@@ -148,7 +152,7 @@ private fun InnerReactionRow(
modifier = ReactionRowExpandButton
) {
Row(verticalAlignment = CenterVertically) {
ExpandButton(baseNote) {
WatchReactionsZapsBoostsAndDisplayIfExists(baseNote) {
RenderShowIndividualReactionsButton(wantsToSeeReactions)
}
}
@@ -163,6 +167,7 @@ private fun InnerReactionRow(
ReplyReactionWithDialog(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav)
}
}
Column(
verticalArrangement = Arrangement.Center,
modifier = remember { Modifier.weight(1f) }
@@ -171,6 +176,7 @@ private fun InnerReactionRow(
BoostWithDialog(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav)
}
}
Column(
verticalArrangement = Arrangement.Center,
modifier = remember { Modifier.weight(1f) }
@@ -179,6 +185,7 @@ private fun InnerReactionRow(
LikeReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel)
}
}
Column(
verticalArrangement = Arrangement.Center,
modifier = remember { Modifier.weight(1f) }
@@ -187,6 +194,7 @@ private fun InnerReactionRow(
ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel)
}
}
Column(
verticalArrangement = Arrangement.Center,
modifier = remember { Modifier.weight(1f) }
@@ -217,13 +225,12 @@ private fun LoadAndDisplayZapraiser(
if (zapraiserAmount > 0) {
Spacer(modifier = Height4dpModifier)
Row(
verticalAlignment = CenterVertically,
Box(
modifier = remember {
ReactionRowZapraiserSize
.padding(start = if (showReactionDetail) Size75dp else Size0dp)
},
horizontalArrangement = Arrangement.Start
contentAlignment = CenterStart
) {
RenderZapRaiser(baseNote, zapraiserAmount, wantsToSeeReactions.value, accountViewModel)
}
@@ -272,7 +279,7 @@ fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, acc
}
LinearProgressIndicator(
modifier = ReactionRowZapraiserSize,
modifier = Modifier.fillMaxWidth().height(if (details) 24.dp else 4.dp),
color = color,
progress = zapraiserProgress
)
@@ -292,29 +299,52 @@ fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, acc
text = stringResource(id = R.string.sats_to_complete, totalPercentage, zapraiserLeft),
modifier = NoSoTinyBorders,
color = MaterialTheme.colors.placeholderText,
fontSize = Font14SP
fontSize = Font14SP,
maxLines = 1
)
}
}
}
@Composable
private fun ExpandButton(baseNote: Note, content: @Composable () -> Unit) {
val zapsState by baseNote.live().zaps.observeAsState()
val boostsState by baseNote.live().boosts.observeAsState()
val reactionsState by baseNote.live().reactions.observeAsState()
private fun WatchReactionsZapsBoostsAndDisplayIfExists(baseNote: Note, content: @Composable () -> Unit) {
val hasReactions by baseNote.live().zaps.combineWith(
liveData1 = baseNote.live().boosts,
liveData2 = baseNote.live().reactions,
block = { zapsState, boostsState, reactionsState ->
zapsState?.note?.zaps?.isNotEmpty() == true ||
boostsState?.note?.boosts?.isNotEmpty() == true ||
reactionsState?.note?.reactions?.isNotEmpty() == true
}
).observeAsState(
baseNote.zaps.isNotEmpty() ||
baseNote.boosts.isNotEmpty() ||
baseNote.reactions.isNotEmpty()
)
val hasReactions by remember(zapsState, boostsState, reactionsState) {
derivedStateOf {
baseNote.zaps.isNotEmpty() ||
baseNote.boosts.isNotEmpty() ||
baseNote.reactions.isNotEmpty()
Crossfade(targetState = hasReactions) {
if (it) {
content()
}
}
}
if (hasReactions) {
content()
fun <T, K, P, R> LiveData<T>.combineWith(
liveData1: LiveData<K>,
liveData2: LiveData<P>,
block: (T?, K?, P?) -> R
): LiveData<R> {
val result = MediatorLiveData<R>()
result.addSource(this) {
result.value = block(this.value, liveData1.value, liveData2.value)
}
result.addSource(liveData1) {
result.value = block(this.value, liveData1.value, liveData2.value)
}
result.addSource(liveData2) {
result.value = block(this.value, liveData1.value, liveData2.value)
}
return result
}
@Composable
@@ -325,20 +355,22 @@ private fun RenderShowIndividualReactionsButton(wantsToSeeReactions: MutableStat
},
modifier = Size20Modifier
) {
if (wantsToSeeReactions.value) {
Icon(
imageVector = Icons.Default.ExpandLess,
null,
modifier = Size22Modifier,
tint = MaterialTheme.colors.subtleButton
)
} else {
Icon(
imageVector = Icons.Default.ExpandMore,
null,
modifier = Size22Modifier,
tint = MaterialTheme.colors.subtleButton
)
Crossfade(targetState = wantsToSeeReactions.value) {
if (it) {
Icon(
imageVector = Icons.Default.ExpandLess,
null,
modifier = Size22Modifier,
tint = MaterialTheme.colors.subtleButton
)
} else {
Icon(
imageVector = Icons.Default.ExpandMore,
null,
modifier = Size22Modifier,
tint = MaterialTheme.colors.subtleButton
)
}
}
}
}
@@ -387,7 +419,6 @@ private fun ReactionDetailGallery(
if (hasBoostEvents) {
RenderBoostGallery(
boostEvents,
backgroundColor,
nav,
accountViewModel
)
@@ -399,7 +430,6 @@ private fun ReactionDetailGallery(
RenderLikeGallery(
it.key,
reactions,
backgroundColor,
nav,
accountViewModel
)
@@ -503,7 +533,7 @@ fun ReplyReaction(
fun ReplyCounter(baseNote: Note, textColor: Color) {
val repliesState by baseNote.live().replies.map {
it.note.replies.size
}.observeAsState(0)
}.observeAsState(baseNote.replies.size)
SlidingAnimation(repliesState, textColor)
}
@@ -521,16 +551,21 @@ private fun SlidingAnimation(baseCount: Int, textColor: Color) {
}
}
) { count ->
Text(
text = remember { showCount(count) },
fontSize = Font14SP,
color = textColor,
modifier = HalfStartPadding,
maxLines = 1
)
TextCount(count, textColor)
}
}
@Composable
private fun TextCount(count: Int, textColor: Color) {
Text(
text = remember(count) { showCount(count) },
fontSize = Font14SP,
color = textColor,
modifier = HalfStartPadding,
maxLines = 1
)
}
@Composable
@OptIn(ExperimentalAnimationApi::class)
private fun SlidingAnimation(amount: String, textColor: Color) {
@@ -634,7 +669,7 @@ fun BoostIcon(baseNote: Note, iconSize: Dp = Size20dp, grayTint: Color, accountV
fun BoostText(baseNote: Note, grayTint: Color) {
val boostState by baseNote.live().boosts.map {
it.note.boosts.size
}.distinctUntilChanged().observeAsState(0)
}.distinctUntilChanged().observeAsState(baseNote.boosts.size)
SlidingAnimation(boostState, grayTint)
}
@@ -723,7 +758,9 @@ fun LikeIcon(
launch(Dispatchers.Default) {
val newReactionType = reactionsState?.note?.isReactedBy(accountViewModel.userProfile())?.firstFullChar()
if (reactionType != newReactionType) {
reactionType = newReactionType
launch(Dispatchers.Main) {
reactionType = newReactionType
}
}
}
}
@@ -784,14 +821,16 @@ fun LikeText(baseNote: Note, grayTint: Color) {
val reactionsState by baseNote.live().reactions.observeAsState()
var reactionsCount by remember(baseNote) {
mutableStateOf(0)
mutableStateOf(baseNote.reactions.size)
}
LaunchedEffect(key1 = reactionsState) {
launch(Dispatchers.Default) {
val newReactionsCount = reactionsState?.note?.countReactions() ?: 0
if (reactionsCount != newReactionsCount) {
reactionsCount = newReactionsCount
launch(Dispatchers.Main) {
reactionsCount = newReactionsCount
}
}
}
}
@@ -923,7 +962,7 @@ fun ZapReaction(
}
if (zappingProgress > 0.00001 && zappingProgress < 0.99999) {
Spacer(Modifier.width(3.dp))
Spacer(ModifierWidth3dp)
CircularProgressIndicator(
progress = animateFloatAsState(
@@ -1020,7 +1059,9 @@ private fun ZapIcon(
val newWasZapped = accountViewModel.calculateIfNoteWasZappedByAccount(it)
if (wasZappedByLoggedInUser != newWasZapped) {
wasZappedByLoggedInUser = newWasZapped
launch(Dispatchers.Main) {
wasZappedByLoggedInUser = newWasZapped
}
}
}
}
@@ -1061,7 +1102,9 @@ private fun ZapAmountText(
zapsState?.note?.let {
val newZapAmount = showAmount(accountViewModel.calculateZapAmount(it))
if (newZapAmount != zapAmountTxt) {
zapAmountTxt = newZapAmount
launch(Dispatchers.Main) {
zapAmountTxt = newZapAmount
}
}
}
}

View File

@@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Size55dp
@Composable
fun UserCompose(
@@ -38,9 +39,9 @@ fun UserCompose(
modifier = overallModifier,
verticalAlignment = Alignment.CenterVertically
) {
UserPicture(baseUser, 55.dp, accountViewModel = accountViewModel, nav = nav)
UserPicture(baseUser, Size55dp, accountViewModel = accountViewModel, nav = nav)
Column(modifier = Modifier.padding(start = 10.dp).weight(1f)) {
Column(modifier = remember { Modifier.padding(start = 10.dp).weight(1f) }) {
Row(verticalAlignment = Alignment.CenterVertically) {
UsernameDisplay(baseUser)
}
@@ -48,7 +49,7 @@ fun UserCompose(
AboutDisplay(baseUser)
}
Column(modifier = Modifier.padding(start = 10.dp)) {
Column(modifier = remember { Modifier.padding(start = 10.dp) }) {
UserActionOptions(baseUser, accountViewModel)
}
}

View File

@@ -2,6 +2,8 @@ package com.vitorpamplona.amethyst.ui.note
import android.content.Context
import android.util.Log
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
@@ -20,13 +22,11 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.tts.TextToSpeechHelper
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.theme.StdButtonSizeModifier
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
@@ -36,38 +36,34 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier) {
val authorState by baseNote.live().metadata.map {
it.note.author
}.distinctUntilChanged().observeAsState()
}.observeAsState(baseNote.author)
authorState?.let {
UsernameDisplay(it, weight)
Crossfade(targetState = authorState, modifier = weight) {
it?.let {
UsernameDisplay(it, weight)
}
}
}
@Composable
fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier) {
val userState by baseUser.live().metadata.observeAsState()
val bestUserName by remember(userState) {
derivedStateOf {
userState?.user?.bestUsername()
}
}
val bestDisplayName by remember(userState) {
derivedStateOf {
userState?.user?.bestDisplayName()
}
}
val npubDisplay by remember {
derivedStateOf {
baseUser.pubkeyDisplayHex()
}
}
val tags by remember(userState) {
derivedStateOf {
userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists()
val userMetadata by baseUser.live().metadata.map {
it.user.info
}.observeAsState(baseUser.info)
Crossfade(targetState = userMetadata, modifier = weight) {
if (it != null) {
UserNameDisplay(it.bestUsername(), it.bestDisplayName(), npubDisplay, it.tags, weight)
} else {
NPubDisplay(npubDisplay, weight)
}
}
UserNameDisplay(bestUserName, bestDisplayName, npubDisplay, tags, weight)
}
@Composable
@@ -106,16 +102,18 @@ private fun UserDisplay(
tags: ImmutableListOfLists<String>?,
modifier: Modifier
) {
CreateTextWithEmoji(
text = bestDisplayName,
tags = tags,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = modifier
)
Spacer(StdHorzSpacer)
DrawPlayName(bestDisplayName)
Row(modifier = modifier) {
CreateTextWithEmoji(
text = bestDisplayName,
tags = tags,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = modifier
)
Spacer(StdHorzSpacer)
DrawPlayName(bestDisplayName)
}
}
@Composable
@@ -125,22 +123,24 @@ private fun UserAndUsernameDisplay(
bestUserName: String,
modifier: Modifier
) {
CreateTextWithEmoji(
text = bestDisplayName,
tags = tags,
fontWeight = FontWeight.Bold,
maxLines = 1
)
CreateTextWithEmoji(
text = remember { "@$bestUserName" },
tags = tags,
color = MaterialTheme.colors.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = modifier
)
Spacer(StdHorzSpacer)
DrawPlayName(bestDisplayName)
Row(modifier = modifier) {
CreateTextWithEmoji(
text = bestDisplayName,
tags = tags,
fontWeight = FontWeight.Bold,
maxLines = 1
)
CreateTextWithEmoji(
text = remember { "@$bestUserName" },
tags = tags,
color = MaterialTheme.colors.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = modifier
)
Spacer(StdHorzSpacer)
DrawPlayName(bestDisplayName)
}
}
@Composable

View File

@@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShowUserButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UnfollowButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -106,7 +107,7 @@ private fun RenderZapNote(
},
verticalAlignment = Alignment.CenterVertically
) {
UserPicture(baseAuthor, 55.dp, accountViewModel = accountViewModel, nav = nav)
UserPicture(baseAuthor, Size55dp, accountViewModel = accountViewModel, nav = nav)
Column(
modifier = remember {

View File

@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.screen
import android.util.Log
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.PaddingValues
@@ -18,6 +19,8 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.ui.note.ChatroomCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
@Composable
fun ChatroomListFeedView(
@@ -68,6 +71,7 @@ private fun CrossFadeState(
}
}
@OptIn(ExperimentalTime::class)
@Composable
private fun FeedLoaded(
state: FeedState.Loaded,
@@ -108,11 +112,14 @@ private fun FeedLoaded(
key = { index, item -> if (index == 0) index else item.idHex }
) { _, item ->
Row(Modifier.fillMaxWidth()) {
ChatroomCompose(
item,
accountViewModel = accountViewModel,
nav = nav
)
val (value, elapsed) = measureTimedValue {
ChatroomCompose(
item,
accountViewModel = accountViewModel,
nav = nav
)
}
Log.d("Rendering Metrics", "Chat Header Complete: ${item.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
}
}
}

View File

@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.screen
import android.util.Log
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -35,6 +36,8 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
@Composable
fun RefresheableFeedView(
@@ -175,7 +178,7 @@ private fun WatchScrollToTop(
}
}
@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class)
@Composable
private fun FeedLoaded(
state: FeedState.Loaded,
@@ -196,21 +199,24 @@ private fun FeedLoaded(
state = listState
) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
val defaultModifier = remember {
Modifier
.fillMaxWidth().animateItemPlacement()
val (value, elapsed) = measureTimedValue {
val defaultModifier = remember {
Modifier.fillMaxWidth().animateItemPlacement()
}
Row(defaultModifier) {
NoteCompose(
item,
routeForLastRead = routeForLastRead,
modifier = baseModifier,
isBoostedNote = false,
accountViewModel = accountViewModel,
nav = nav
)
}
}
Row(defaultModifier) {
NoteCompose(
item,
routeForLastRead = routeForLastRead,
modifier = baseModifier,
isBoostedNote = false,
accountViewModel = accountViewModel,
nav = nav
)
}
Log.d("Rendering Metrics", "Complete: ${item.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
}
}
}

View File

@@ -21,6 +21,9 @@ import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.PayInvoiceErrorResponse
import com.vitorpamplona.amethyst.service.model.ReportEvent
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -57,6 +60,11 @@ class AccountViewModel(val account: Account) : ViewModel() {
}
}
fun isNoteHidden(note: Note): Boolean {
val isSensitive = note.event?.isSensitive() ?: false
return account.isHidden(note.author!!) || (isSensitive && account.showSensitiveContent == false)
}
fun hasReactedTo(baseNote: Note, reaction: String): Boolean {
return account.hasReacted(baseNote, reaction)
}
@@ -263,6 +271,29 @@ class AccountViewModel(val account: Account) : ViewModel() {
return account.defaultZapType
}
fun isNoteAcceptable(note: Note, onReady: (Boolean, Boolean, ImmutableSet<Note>) -> Unit) {
val isFromLoggedIn = note.author?.pubkeyHex == userProfile().pubkeyHex
val isFromLoggedInFollow = note.author?.let { userProfile().isFollowingCached(it) } ?: true
if (isFromLoggedIn || isFromLoggedInFollow) {
// No need to process if from trusted people
onReady(true, true, persistentSetOf())
} else {
val newCanPreview = !note.hasAnyReports()
val newIsAcceptable = account.isAcceptable(note)
if (newCanPreview && newIsAcceptable) {
// No need to process reports if nothing is wrong
onReady(true, true, persistentSetOf())
} else {
val newRelevantReports = account.getRelevantReports(note)
onReady(newIsAcceptable, newCanPreview, newRelevantReports.toImmutableSet())
}
}
}
class Factory(val account: Account) : ViewModelProvider.Factory {
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel {
return AccountViewModel(account) as AccountViewModel

View File

@@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.heightIn
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.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
@@ -89,8 +88,8 @@ import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.actions.PostButton
import com.vitorpamplona.amethyst.ui.actions.ServersAvailable
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.VideoView
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlVideo
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
import com.vitorpamplona.amethyst.ui.note.LikeReaction
@@ -117,7 +116,7 @@ fun ChannelScreen(
) {
if (channelId == null) return
var channelBase by remember { mutableStateOf<Channel?>(null) }
var channelBase by remember { mutableStateOf<Channel?>(LocalCache.getChannelIfExists(channelId)) }
LaunchedEffect(channelId) {
withContext(Dispatchers.IO) {
@@ -550,11 +549,14 @@ fun ChannelHeader(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val scope = rememberCoroutineScope()
Column(
Modifier
.fillMaxWidth()
.clickable {
nav("Channel/${baseChannel.idHex}")
scope.launch {
nav("Channel/${baseChannel.idHex}")
}
}
) {
val channelState by baseChannel.live.observeAsState()
@@ -562,15 +564,21 @@ fun ChannelHeader(
val streamingUrl by remember(channelState) {
derivedStateOf {
(channel as? LiveActivitiesChannel)?.info?.streaming()
val activity = channel as? LiveActivitiesChannel
val description = activity?.info?.title()
val url = activity?.info?.streaming()
if (url != null) {
ZoomableUrlVideo(url, description = description)
} else {
null
}
}
}
if (streamingUrl != null && showVideo) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = remember { Modifier.heightIn(max = 300.dp) }) {
VideoView(
videoUri = streamingUrl!!,
description = null
ZoomableContentView(
content = streamingUrl!!
)
}
}
@@ -586,16 +594,20 @@ fun ChannelHeader(
)
}
RobohashAsyncImageProxy(
robot = channel.idHex,
model = channel.profilePicture(),
contentDescription = stringResource(R.string.profile_image),
modifier = Modifier
.width(Size35dp)
.height(Size35dp)
.padding(start = 10.dp)
.clip(shape = CircleShape)
)
/*
channel.profilePicture()?.let {
RobohashAsyncImageProxy(
robot = channel.idHex,
model = it,
contentDescription = stringResource(R.string.profile_image),
contentScale = ContentScale.Crop,
modifier = Modifier.padding(start = 10.dp)
.width(Size35dp)
.height(Size35dp)
.clip(shape = CircleShape)
)
}
*/
Column(
modifier = Modifier
@@ -632,7 +644,7 @@ fun ChannelHeader(
Row(
modifier = Modifier
.height(Size35dp)
.padding(bottom = 3.dp),
.padding(start = 5.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (channel is PublicChatChannel) {

View File

@@ -34,8 +34,8 @@ import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.material.MaterialRichText
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.components.richTextDefaults
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.RichTextDefaults
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.launch
@@ -86,8 +86,8 @@ fun ConnectOrbotDialog(onClose: () -> Unit, onPost: () -> Unit, portNumber: Muta
Column(
modifier = Modifier.padding(30.dp)
) {
val myMarkDownStyle = richTextDefaults.copy(
stringStyle = richTextDefaults.stringStyle?.copy(
val myMarkDownStyle = RichTextDefaults.copy(
stringStyle = RichTextDefaults.stringStyle?.copy(
linkStyle = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colors.primary

View File

@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -85,8 +86,10 @@ fun HashtagScreen(tag: String?, accountViewModel: AccountViewModel, nav: (String
}
@Composable
fun HashtagHeader(tag: String, account: AccountViewModel) {
Column() {
fun HashtagHeader(tag: String, account: AccountViewModel, onClick: () -> Unit = { }) {
Column(
Modifier.clickable { onClick() }
) {
Column(modifier = Modifier.padding(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Column(

View File

@@ -189,9 +189,7 @@ private fun FeedLoaded(
val listState = rememberLazyListState()
LazyColumn(
contentPadding = PaddingValues(
top = 10.dp
),
contentPadding = PaddingValues(),
state = listState
) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
@@ -222,8 +220,10 @@ fun CheckIfLiveActivityIsOnline(note: Note, whenOnline: @Composable () -> Unit)
}
}
if (online) {
whenOnline()
Crossfade(targetState = online) {
if (it) {
whenOnline()
}
}
}

View File

@@ -426,8 +426,9 @@ private fun CreateAndRenderPages(
3 -> TabFollowers(baseUser, followersFeedViewModel, accountViewModel, nav)
4 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav)
5 -> TabBookmarks(bookmarksFeedViewModel, accountViewModel, nav)
6 -> TabReports(baseUser, reportsFeedViewModel, accountViewModel, nav)
7 -> TabRelays(baseUser, accountViewModel, nav)
6 -> TabFollowedTags(baseUser, accountViewModel, nav)
7 -> TabReports(baseUser, reportsFeedViewModel, accountViewModel, nav)
8 -> TabRelays(baseUser, accountViewModel, nav)
}
}
@@ -446,6 +447,7 @@ private fun CreateAndRenderTabs(
{ FollowersTabHeader(baseUser) },
{ ZapTabHeader(baseUser) },
{ BookmarkTabHeader(baseUser) },
{ FollowedTagsTabHeader(baseUser) },
{ ReportsTabHeader(baseUser) },
{ RelaysTabHeader(baseUser) }
)
@@ -490,6 +492,25 @@ private fun ReportsTabHeader(baseUser: User) {
Text(text = "$userReports ${stringResource(R.string.reports)}")
}
@Composable
private fun FollowedTagsTabHeader(baseUser: User) {
var usertags by remember { mutableStateOf(0) }
LaunchedEffect(key1 = baseUser) {
launch(Dispatchers.IO) {
val contactList = baseUser?.latestContactList
val newTags = (contactList?.verifiedFollowTagSet?.count() ?: 0)
if (newTags != usertags) {
usertags = newTags
}
}
}
Text(text = "$usertags ${stringResource(R.string.followed_tags)}")
}
@Composable
private fun BookmarkTabHeader(baseUser: User) {
val userState by baseUser.live().bookmarks.observeAsState()
@@ -1277,6 +1298,27 @@ fun TabNotesConversations(feedViewModel: NostrUserProfileConversationsFeedViewMo
}
}
@Composable
fun TabFollowedTags(baseUser: User, account: AccountViewModel, nav: (String) -> Unit) {
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
baseUser.latestContactList?.let {
it.unverifiedFollowTagSet().forEach { hashtag ->
HashtagHeader(
tag = hashtag,
account = account,
onClick = {
nav("Hashtag/$hashtag")
}
)
}
}
}
}
}
@Composable
fun TabBookmarks(feedViewModel: NostrUserProfileBookmarksFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
LaunchedEffect(Unit) {

View File

@@ -3,9 +3,11 @@ package com.vitorpamplona.amethyst.ui.theme
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
@@ -19,6 +21,8 @@ val Shapes = Shapes(
large = RoundedCornerShape(0.dp)
)
val RippleRadius45dp = 45.dp // Ripple should be +10.dp over the component size
val SmallBorder = RoundedCornerShape(7.dp)
val QuoteBorder = RoundedCornerShape(15.dp)
val ButtonBorder = RoundedCornerShape(20.dp)
@@ -39,6 +43,7 @@ val DoubleVertSpacer = Modifier.height(10.dp)
val HalfDoubleVertSpacer = Modifier.height(7.dp)
val Size0dp = 0.dp
val Size10dp = 10.dp
val Size13dp = 13.dp
val Size15dp = 15.dp
val Size16dp = 16.dp
@@ -55,14 +60,20 @@ val Size75dp = 75.dp
val HalfStartPadding = Modifier.padding(start = 5.dp)
val StdStartPadding = Modifier.padding(start = 10.dp)
val StdTopPadding = Modifier.padding(top = 10.dp)
val HalfPadding = Modifier.padding(5.dp)
val StdPadding = Modifier.padding(10.dp)
val Size6Modifier = Modifier.size(6.dp)
val Size10Modifier = Modifier.size(10.dp)
val Size15Modifier = Modifier.size(15.dp)
val Size18Modifier = Modifier.size(18.dp)
val Size20Modifier = Modifier.size(20.dp)
val Size22Modifier = Modifier.size(22.dp)
val Size24Modifier = Modifier.size(24.dp)
val Size30Modifier = Modifier.size(30.dp)
val Size35Modifier = Modifier.size(35.dp)
val Size55Modifier = Modifier.size(55.dp)
val TinyBorders = Modifier.padding(2.dp)
@@ -71,11 +82,14 @@ val ReactionRowZapraiserSize = Modifier.defaultMinSize(minHeight = 4.dp).fillMax
val ReactionRowExpandButton = Modifier.width(65.dp).padding(start = 31.dp)
val WidthAuthorPictureModifier = Modifier.width(55.dp)
val WidthAuthorPictureModifierWithPadding = Modifier.width(65.dp)
val DiviserThickness = 0.25.dp
val DividerThickness = 0.25.dp
val ReactionRowHeight = Modifier.height(24.dp).padding(start = 10.dp)
val ReactionRowHeightChat = Modifier.height(25.dp)
val UserNameRowHeight = Modifier.height(22.dp).fillMaxWidth()
val UserNameMaxRowHeight = Modifier.heightIn(max = 22.dp).fillMaxWidth()
val Height4dpModifier = Modifier.height(4.dp)
@@ -86,3 +100,11 @@ val ShowMoreRelaysButtonIconModifier = Modifier.size(15.dp)
val ShowMoreRelaysButtonBoxModifer = Modifier.fillMaxWidth().height(25.dp)
val ChatBubbleMaxSizeModifier = Modifier.fillMaxWidth(0.85f)
val ModifierWidth3dp = Modifier.width(3.dp)
val NotificationIconModifier = Modifier.width(55.dp).padding(end = 5.dp)
val NotificationIconModifierSmaller = Modifier.width(55.dp).padding(end = 4.dp)
val ZapPictureCommentModifier = Modifier.height(35.dp).widthIn(min = 35.dp)
val ChatHeadlineBorders = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp)

View File

@@ -5,6 +5,7 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Colors
import androidx.compose.material.MaterialTheme
@@ -25,9 +26,7 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import com.halilibo.richtext.ui.RichTextStyle
import com.vitorpamplona.amethyst.ui.components.DefaultHeadingStyle
import com.vitorpamplona.amethyst.ui.components.DefaultParagraphSpacing
import com.vitorpamplona.amethyst.ui.components.richTextDefaults
import com.halilibo.richtext.ui.resolveDefaults
private val DarkColorPalette = darkColors(
primary = Purple200,
@@ -121,10 +120,62 @@ val LightImageModifier = Modifier
QuoteBorder
)
val MarkDownStyleOnDark = richTextDefaults.copy(
val DarkProfile35dpModifier = Modifier
.size(Size35dp)
.clip(shape = CircleShape)
.drawBehind { drawRect(DarkColorPalette.background) }
val LightProfile35dpModifier = Modifier
.fillMaxWidth()
.clip(shape = CircleShape)
.drawBehind { drawRect(LightColorPalette.background) }
val DarkReplyBorderModifier = Modifier
.padding(top = 5.dp)
.fillMaxWidth()
.clip(shape = QuoteBorder)
.border(
1.dp,
DarkSubtleBorder,
QuoteBorder
)
val LightReplyBorderModifier = Modifier
.padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp)
.fillMaxWidth()
.clip(shape = QuoteBorder)
.border(
1.dp,
LightSubtleBorder,
QuoteBorder
)
val DarkInnerPostBorderModifier = Modifier
.padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp)
.fillMaxWidth()
.clip(shape = QuoteBorder)
.border(
1.dp,
DarkSubtleBorder,
QuoteBorder
)
val LightInnerPostBorderModifier = Modifier
.padding(top = 5.dp)
.fillMaxWidth()
.clip(shape = QuoteBorder)
.border(
1.dp,
LightSubtleBorder,
QuoteBorder
)
val RichTextDefaults = RichTextStyle().resolveDefaults()
val MarkDownStyleOnDark = RichTextDefaults.copy(
paragraphSpacing = DefaultParagraphSpacing,
headingStyle = DefaultHeadingStyle,
codeBlockStyle = richTextDefaults.codeBlockStyle?.copy(
codeBlockStyle = RichTextDefaults.codeBlockStyle?.copy(
textStyle = TextStyle(
fontFamily = FontFamily.Monospace,
fontSize = Font14SP
@@ -142,7 +193,7 @@ val MarkDownStyleOnDark = richTextDefaults.copy(
drawRect(DarkColorPalette.onSurface.copy(alpha = 0.05f))
}
),
stringStyle = richTextDefaults.stringStyle?.copy(
stringStyle = RichTextDefaults.stringStyle?.copy(
linkStyle = SpanStyle(
color = DarkColorPalette.primary
),
@@ -154,10 +205,10 @@ val MarkDownStyleOnDark = richTextDefaults.copy(
)
)
val MarkDownStyleOnLight = richTextDefaults.copy(
val MarkDownStyleOnLight = RichTextDefaults.copy(
paragraphSpacing = DefaultParagraphSpacing,
headingStyle = DefaultHeadingStyle,
codeBlockStyle = richTextDefaults.codeBlockStyle?.copy(
codeBlockStyle = RichTextDefaults.codeBlockStyle?.copy(
textStyle = TextStyle(
fontFamily = FontFamily.Monospace,
fontSize = Font14SP
@@ -175,7 +226,7 @@ val MarkDownStyleOnLight = richTextDefaults.copy(
drawRect(LightColorPalette.onSurface.copy(alpha = 0.05f))
}
),
stringStyle = richTextDefaults.stringStyle?.copy(
stringStyle = RichTextDefaults.stringStyle?.copy(
linkStyle = SpanStyle(
color = LightColorPalette.primary
),
@@ -243,6 +294,15 @@ val Colors.repostProfileBorder: Modifier
val Colors.imageModifier: Modifier
get() = if (isLight) LightImageModifier else DarkImageModifier
val Colors.profile35dpModifier: Modifier
get() = if (isLight) LightProfile35dpModifier else DarkProfile35dpModifier
val Colors.replyModifier: Modifier
get() = if (isLight) LightReplyBorderModifier else DarkReplyBorderModifier
val Colors.innerPostModifier: Modifier
get() = if (isLight) LightInnerPostBorderModifier else DarkInnerPostBorderModifier
@Composable
fun AmethystTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
val colors = if (darkTheme) {

View File

@@ -4,8 +4,10 @@ import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import com.halilibo.richtext.ui.HeadingStyle
// Set of Material typography styles to start with
val Typography = Typography(
@@ -29,5 +31,37 @@ val Typography = Typography(
)
val Font14SP = 14.sp
val Font17SP = 17.sp
val MarkdownTextStyle = TextStyle(lineHeight = 1.30.em)
val DefaultParagraphSpacing: TextUnit = 12.sp
internal val DefaultHeadingStyle: HeadingStyle = { level, textStyle ->
when (level) {
0 -> textStyle.copy(
fontSize = 30.sp,
fontWeight = FontWeight.Light
)
1 -> textStyle.copy(
fontSize = 26.sp,
fontWeight = FontWeight.Light
)
2 -> textStyle.copy(
fontSize = 22.sp,
fontWeight = FontWeight.Light
)
3 -> textStyle.copy(
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
4 -> textStyle.copy(
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
5 -> textStyle.copy(
fontWeight = FontWeight.Bold
)
else -> textStyle
}
}

View File

@@ -1,30 +1,37 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="192.62dp"
android:height="288.93dp"
android:viewportWidth="192.62"
android:viewportHeight="288.93">
<group>
<clip-path
android:pathData="m111.18,103.42c-4.68,-0 -8.47,-3.8 -8.47,-8.48 0,-4.68 3.79,-8.48 8.47,-8.48 4.68,-0 8.48,3.8 8.48,8.48 0,4.68 -3.79,8.48 -8.48,8.48zM96.31,-0 L0,91.3L0,197.63l19.9,18.86c0.63,-43.4 14.08,-106.43 37.41,-146.62 0,-0 6.05,5.23 16.74,5.93 11.02,0.73 36.71,-4.37 48.32,0.36 10.87,4.43 11.66,17.34 10.79,27.84 -1.66,20.02 8.14,25.99 25.54,34.66 8.67,4.32 10.67,9.28 7.87,14.44 -3.09,5.68 -17.49,3.05 -33.55,1.1 -17.16,-2.08 -26.79,9.45 -43.37,8.39 -23.4,30.62 -0.84,73.09 -16.14,104.73l22.81,21.62 96.31,-91.31l0,-106.32z"/>
<path
android:pathData="m111.18,103.42c-4.68,-0 -8.47,-3.8 -8.47,-8.48 0,-4.68 3.79,-8.48 8.47,-8.48 4.68,-0 8.48,3.8 8.48,8.48 0,4.68 -3.79,8.48 -8.48,8.48zM96.31,-0 L0,91.3L0,197.63l19.9,18.86c0.63,-43.4 14.08,-106.43 37.41,-146.62 0,-0 6.05,5.23 16.74,5.93 11.02,0.73 36.71,-4.37 48.32,0.36 10.87,4.43 11.66,17.34 10.79,27.84 -1.66,20.02 8.14,25.99 25.54,34.66 8.67,4.32 10.67,9.28 7.87,14.44 -3.09,5.68 -17.49,3.05 -33.55,1.1 -17.16,-2.08 -26.79,9.45 -43.37,8.39 -23.4,30.62 -0.84,73.09 -16.14,104.73L96.31,288.93 192.62,197.63L192.62,91.3L96.31,-0"
android:strokeWidth="0.04"
android:strokeColor="#00000000"
android:fillType="nonZero">
<aapt:attr name="android:fillColor">
<gradient
android:startX="33.75"
android:startY="252.82"
android:endX="158.87"
android:endY="36.12"
android:tileMode="clamp"
android:type="linear">
<item android:offset="0" android:color="#FF550066"/>
<item android:offset="0" android:color="#FF550066"/>
<item android:offset="1" android:color="#FFA75CCC"/>
</gradient>
</aapt:attr>
</path>
</group>
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:pathData="M242.66,157.32a15.48,15.75 90,1 0,31.5 0a15.48,15.75 90,1 0,-31.5 0z"
android:strokeWidth="2.74">
<aapt:attr name="android:fillColor">
<gradient
android:startX="42.27"
android:startY="20.13"
android:endX="55.73"
android:endY="20.13"
android:type="linear">
<item android:offset="0" android:color="#FF652D80"/>
<item android:offset="1" android:color="#FF2598CF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M489.48,391.59C488.93,389.92 360.91,85.09 325.7,0.54c-42.87,0 -133.4,0 -197.26,0 -18.74,46.35 -67.12,164.49 -105.93,259.66 21.27,50.74 40.37,96.32 55.1,131.48l80.32,0a5.49,5.49 0,0 0,5.13 -6.45c-0.69,-16.22 -44.84,-75.17 -31.67,-138.94 3.51,-13.72 6.2,-27.66 10.04,-41.33a482.36,482.36 135,0 1,36.88 -95.5c2.44,-4.64 3.57,-6.72 9.19,-3.21 11.99,7.46 25.38,7.03 38.72,6.06 11.5,-2.5 17.1,-8.01 38.17,-2.41 4.17,0 8.23,0 12.49,0.38 10.13,0.8 19.62,3.21 25.71,12.38 6.64,10.02 7.71,21.35 6.64,32.93 -1.62,17.32 -0.47,33.45 14.19,45.66a157.85,157.85 0,0 0,16.47 10.98c7.27,4.67 16.05,6.7 22.28,13.17 3.68,3.81 5.85,7.88 4.25,13.31 -1.59,5.43 -5.85,7.99 -11.44,8.59 -14.16,1.54 -27.99,-1.34 -41.9,-3.02 -1.81,-0.22 -3.59,-0.36 -5.49,-0.47a31.48,31.48 0,0 1,-10.46 0.36l-3.27,0a73.27,73.27 0,0 0,-16.19 3.87c-13.12,4.78 -25.06,10.04 -40.53,9.77a11.86,11.86 0,0 0,-5.63 2.44c-12.13,10.78 -19.43,24.4 -13.36,44.29 16.63,46.46 59.3,158.54 77.64,206.92 -0.52,-32.77 -0.66,-91.44 -0.77,-119.92 73.22,0.11 185.48,0.05 194.43,0.05z"
android:strokeWidth="2.74">
<aapt:attr name="android:fillColor">
<gradient
android:startX="22.6"
android:startY="255.92"
android:endX="489.54"
android:endY="255.92"
android:type="linear">
<item android:offset="0" android:color="#FF652D80"/>
<item android:offset="1" android:color="#FF2598CF"/>
</gradient>
</aapt:attr>
</path>
</vector>

View File

@@ -1,7 +1,15 @@
<vector android:height="108dp" android:viewportHeight="216"
android:viewportWidth="216" android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000000"
android:pathData="M90.2,73.6l-17.2,16.6 0,19.7 0,19.7 3,2.9c1.6,1.6 3.3,2.6 3.6,2.2 0.4,-0.4 1,-4.2 1.5,-8.4 0.9,-9.6 4.5,-25.4 6.9,-30.8 0.5,-1.1 1.9,-4.4 3.1,-7.3 1.9,-4.6 2.4,-5.2 4.3,-4.5 1.1,0.5 5.5,0.7 9.6,0.5 10.8,-0.3 13.7,0.3 15.5,3.8 0.8,1.6 1.5,5 1.5,7.5 0,6.1 1,7.7 7,11.4 8,4.9 5.6,8.7 -4.4,6.8 -3.9,-0.7 -6.2,-0.5 -9.6,0.6 -2.5,0.9 -5.8,1.8 -7.3,2.2 -4.4,0.9 -5.7,5.3 -5.7,19.8 0,6.8 -0.5,13.9 -1.1,15.7 -1.1,3 -1,3.4 2.9,7.3l4.1,4.1 5.8,-5.4c3.2,-3 11.3,-10.7 18.1,-17.2l12.2,-11.9 0,-18.9 0,-19 -13.2,-12.8c-14.2,-13.7 -22.3,-21.2 -22.9,-21.2 -0.2,-0 -8.2,7.5 -17.7,16.6z" android:strokeColor="#00000000"/>
<path android:fillColor="#000000"
android:pathData="M111,91.5c0,3.1 2.6,4.3 4.6,2.3 2,-2 0.7,-4.8 -2.2,-4.8 -1.9,-0 -2.4,0.5 -2.4,2.5z" android:strokeColor="#00000000"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#000000"
android:pathData="M249.48,212.29a7.57,7.7 90,1 0,15.41 0a7.57,7.7 90,1 0,-15.41 0z"
android:strokeWidth="1.34">
</path>
<path android:fillColor="#000000"
android:pathData="M370.18,326.86C369.91,326.04 307.3,176.97 290.09,135.63c-20.96,0 -65.23,0 -96.46,0 -9.17,22.67 -32.82,80.44 -51.8,126.98 10.4,24.81 19.74,47.1 26.95,64.29l39.28,0a2.68,2.68 135,0 0,2.51 -3.15c-0.34,-7.93 -21.93,-36.76 -15.49,-67.94 1.72,-6.71 3.03,-13.53 4.91,-20.21a235.88,235.88 135,0 1,18.04 -46.7c1.19,-2.27 1.74,-3.29 4.5,-1.57 5.86,3.65 12.41,3.44 18.94,2.97 5.62,-1.22 8.36,-3.92 18.67,-1.18 2.04,0 4.03,0 6.11,0.19 4.95,0.39 9.6,1.57 12.57,6.05 3.25,4.9 3.77,10.44 3.25,16.1 -0.79,8.47 -0.23,16.36 6.94,22.33a77.19,77.19 0,0 0,8.05 5.37c3.56,2.28 7.85,3.27 10.9,6.44 1.8,1.87 2.86,3.85 2.08,6.51 -0.78,2.66 -2.86,3.91 -5.6,4.2 -6.92,0.75 -13.69,-0.66 -20.49,-1.48 -0.89,-0.11 -1.76,-0.17 -2.68,-0.23a15.39,15.39 0,0 1,-5.11 0.17l-1.6,0a35.83,35.83 0,0 0,-7.92 1.89c-6.41,2.34 -12.25,4.91 -19.82,4.78a5.8,5.8 0,0 0,-2.75 1.19c-5.93,5.27 -9.5,11.93 -6.54,21.66 8.13,22.72 29,77.53 37.96,101.18 -0.25,-16.02 -0.32,-44.71 -0.38,-58.64 35.8,0.05 90.7,0.03 95.08,0.03z"
android:strokeWidth="1.34">
</path>
</vector>

File diff suppressed because one or more lines are too long

View File

@@ -1,35 +1,37 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="192.62"
android:viewportHeight="288.93">
<group android:scaleX="0.33333334"
android:scaleY="0.5"
android:translateX="64.206665"
android:translateY="72.2325">
<group>
<clip-path
android:pathData="m111.18,103.42c-4.68,-0 -8.47,-3.8 -8.47,-8.48 0,-4.68 3.79,-8.48 8.47,-8.48 4.68,-0 8.48,3.8 8.48,8.48 0,4.68 -3.79,8.48 -8.48,8.48zM96.31,-0 L0,91.3L0,197.63l19.9,18.86c0.63,-43.4 14.08,-106.43 37.41,-146.62 0,-0 6.05,5.23 16.74,5.93 11.02,0.73 36.71,-4.37 48.32,0.36 10.87,4.43 11.66,17.34 10.79,27.84 -1.66,20.02 8.14,25.99 25.54,34.66 8.67,4.32 10.67,9.28 7.87,14.44 -3.09,5.68 -17.49,3.05 -33.55,1.1 -17.16,-2.08 -26.79,9.45 -43.37,8.39 -23.4,30.62 -0.84,73.09 -16.14,104.73l22.81,21.62 96.31,-91.31l0,-106.32z"/>
<path
android:pathData="m111.18,103.42c-4.68,-0 -8.47,-3.8 -8.47,-8.48 0,-4.68 3.79,-8.48 8.47,-8.48 4.68,-0 8.48,3.8 8.48,8.48 0,4.68 -3.79,8.48 -8.48,8.48zM96.31,-0 L0,91.3L0,197.63l19.9,18.86c0.63,-43.4 14.08,-106.43 37.41,-146.62 0,-0 6.05,5.23 16.74,5.93 11.02,0.73 36.71,-4.37 48.32,0.36 10.87,4.43 11.66,17.34 10.79,27.84 -1.66,20.02 8.14,25.99 25.54,34.66 8.67,4.32 10.67,9.28 7.87,14.44 -3.09,5.68 -17.49,3.05 -33.55,1.1 -17.16,-2.08 -26.79,9.45 -43.37,8.39 -23.4,30.62 -0.84,73.09 -16.14,104.73L96.31,288.93 192.62,197.63L192.62,91.3L96.31,-0"
android:strokeWidth="0.04"
android:strokeColor="#00000000"
android:fillType="nonZero">
<aapt:attr name="android:fillColor">
<gradient
android:startX="33.75"
android:startY="252.82"
android:endX="158.87"
android:endY="36.12"
android:tileMode="clamp"
android:type="linear">
<item android:offset="0" android:color="#FF550066"/>
<item android:offset="0" android:color="#FF550066"/>
<item android:offset="1" android:color="#FFA75CCC"/>
</gradient>
</aapt:attr>
</path>
</group>
</group>
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:pathData="M249.48,212.29a7.57,7.7 90,1 0,15.41 0a7.57,7.7 90,1 0,-15.41 0z"
android:strokeWidth="1.34">
<aapt:attr name="android:fillColor">
<gradient
android:startX="151.49"
android:startY="145.21"
android:endX="158.07"
android:endY="145.21"
android:type="linear">
<item android:offset="0" android:color="#FF652D80"/>
<item android:offset="1" android:color="#FF2598CF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M370.18,326.86C369.91,326.04 307.3,176.97 290.09,135.63c-20.96,0 -65.23,0 -96.46,0 -9.17,22.67 -32.82,80.44 -51.8,126.98 10.4,24.81 19.74,47.1 26.95,64.29l39.28,0a2.68,2.68 135,0 0,2.51 -3.15c-0.34,-7.93 -21.93,-36.76 -15.49,-67.94 1.72,-6.71 3.03,-13.53 4.91,-20.21a235.88,235.88 135,0 1,18.04 -46.7c1.19,-2.27 1.74,-3.29 4.5,-1.57 5.86,3.65 12.41,3.44 18.94,2.97 5.62,-1.22 8.36,-3.92 18.67,-1.18 2.04,0 4.03,0 6.11,0.19 4.95,0.39 9.6,1.57 12.57,6.05 3.25,4.9 3.77,10.44 3.25,16.1 -0.79,8.47 -0.23,16.36 6.94,22.33a77.19,77.19 0,0 0,8.05 5.37c3.56,2.28 7.85,3.27 10.9,6.44 1.8,1.87 2.86,3.85 2.08,6.51 -0.78,2.66 -2.86,3.91 -5.6,4.2 -6.92,0.75 -13.69,-0.66 -20.49,-1.48 -0.89,-0.11 -1.76,-0.17 -2.68,-0.23a15.39,15.39 0,0 1,-5.11 0.17l-1.6,0a35.83,35.83 0,0 0,-7.92 1.89c-6.41,2.34 -12.25,4.91 -19.82,4.78a5.8,5.8 0,0 0,-2.75 1.19c-5.93,5.27 -9.5,11.93 -6.54,21.66 8.13,22.72 29,77.53 37.96,101.18 -0.25,-16.02 -0.32,-44.71 -0.38,-58.64 35.8,0.05 90.7,0.03 95.08,0.03z"
android:strokeWidth="1.34">
<aapt:attr name="android:fillColor">
<gradient
android:startX="141.87"
android:startY="260.51"
android:endX="370.2"
android:endY="260.51"
android:type="linear">
<item android:offset="0" android:color="#FF652D80"/>
<item android:offset="1" android:color="#FF2598CF"/>
</gradient>
</aapt:attr>
</path>
</vector>

View File

@@ -0,0 +1,31 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="600dp"
android:height="600dp"
android:viewportWidth="600"
android:viewportHeight="600">
<path
android:pathData="m171.93,82.63 l405.17,-82.63 0,112.99 -405.17,82.63z"
android:fillColor="#712F8F"
android:strokeColor="#00000000"
android:fillType="nonZero"/>
<path
android:pathData="M171.93,82.54l65.23,0l0,422.37l-65.23,0z"
android:fillColor="#712F8F"
android:strokeColor="#00000000"
android:fillType="nonZero"/>
<path
android:pathData="m235.04,488.2c11.66,43.5 -25.91,91.36 -83.91,106.9 -58,15.54 -114.46,-7.12 -126.12,-50.62 -11.66,-43.5 25.91,-91.36 83.91,-106.9 58,-15.54 114.46,7.12 126.12,50.62z"
android:fillColor="#712F8F"
android:strokeColor="#00000000"
android:fillType="nonZero"/>
<path
android:pathData="M511.87,13.54l65.23,0l0,422.37l-65.23,0z"
android:fillColor="#712F8F"
android:strokeColor="#00000000"
android:fillType="nonZero"/>
<path
android:pathData="m574.98,419.21c11.66,43.5 -25.91,91.36 -83.91,106.9 -58,15.54 -114.46,-7.12 -126.12,-50.62 -11.66,-43.5 25.91,-91.36 83.91,-106.9 58,-15.54 114.46,7.12 126.12,50.62z"
android:fillColor="#712F8F"
android:strokeColor="#00000000"
android:fillType="nonZero"/>
</vector>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/amethyst_monochrome"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -323,6 +323,9 @@
<string name="upload_server_nostrfilesdev">nostrfiles.dev důvěryhodný</string>
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev může upravit soubor</string>
<string name="upload_server_nostrcheckme">nostrcheck.me - důvěryhodný</string>
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me může upravit soubor</string>
<string name="upload_server_imgur_nip94">Ověřitelný Imgur (NIP-94)</string>
<string name="upload_server_imgur_nip94_explainer">Ověří, zda Imgur upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
@@ -335,6 +338,9 @@
<string name="upload_server_nostrfilesdev_nip94">Ověřitelný Nostrfiles.dev (NIP-94)</string>
<string name="upload_server_nostrfilesdev_nip94_explainer">Ověří, zda Nostrfiles.dev upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
<string name="upload_server_nostrcheckme_nip94">Ověřitelný Nostrcheck.me (NIP-94)</string>
<string name="upload_server_nostrcheckme_nip94_explainer">Ověří, zda Nostrcheck.me upravil soubor. Nový NIP: jiní klienti ho možná neuvidí</string>
<string name="upload_server_relays_nip95">Vaše relé (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">Soubory jsou hostovány vašimi relé. Nový NIP: zkontrolujte, zda je podporují</string>
@@ -387,4 +393,53 @@
<string name="content_warning_hide_all_sensitive_content">Vždy skrýt citlivý obsah</string>
<string name="content_warning_show_all_sensitive_content">Vždy zobrazit citlivý obsah</string>
<string name="content_warning_see_warnings">Vždy zobrazovat upozornění na obsah</string>
<string name="recommended_apps">Doporučuje: </string>
<string name="filter_spam_from_strangers">Filtrovat spam od cizinců</string>
<string name="warn_when_posts_have_reports_from_your_follows">Varovat, když příspěvky obsahují hlášení od vašich sledovaných osob</string>
<string name="new_reaction_symbol">Nový symbol reakce</string>
<string name="no_reaction_type_setup_long_press_to_change">Nebyly vybrány žádné typy reakcí. Dlouhým stiskem změňte</string>
<string name="zapraiser">Zapraiser</string>
<string name="zapraiser_explainer">Přidá cílovou částku sats pro tento příspěvek. Podporované klienty ji mohou zobrazovat jako ukazatel postupu k podněcování darování</string>
<string name="zapraiser_target_amount_in_sats">Cílová částka v sats</string>
<string name="sats_to_complete">Zapraiser na %1$s. Do cíle zbývá %2$s sats</string>
<string name="read_from_relay">Číst z Relay</string>
<string name="write_to_relay">Zapisovat do Relay</string>
<string name="an_error_ocurred_trying_to_get_relay_information">Při pokusu o získání informací z Relay se vyskytla chyba z %1$s</string>
<string name="owner">Vlastník</string>
<string name="version">Verze</string>
<string name="software">Software</string>
<string name="contact">Kontakt</string>
<string name="supports">Podporované NIPs</string>
<string name="admission_fees">Vstupné</string>
<string name="payments_url">URL plateb</string>
<string name="limitations">Omezení</string>
<string name="countries">Země</string>
<string name="languages">Jazyky</string>
<string name="tags">Tagy</string>
<string name="posting_policy">Politika příspěvků</string>
<string name="message_length">Délka zprávy</string>
<string name="subscriptions">Předplatné</string>
<string name="filters">Filtry</string>
<string name="subscription_id_length">Délka ID předplatného</string>
<string name="minimum_prefix">Minimální předpona</string>
<string name="maximum_event_tags">Maximální počet značek události</string>
<string name="content_length">Délka obsahu</string>
<string name="minimum_pow">Minimální PoW</string>
<string name="auth">Ověření</string>
<string name="payment">Platba</string>
<string name="cashu">Cashu Token</string>
<string name="cashu_redeem">Vyměnit</string>
<string name="live_stream_live_tag">ŽIVĚ</string>
<string name="live_stream_offline_tag">OFFLINE</string>
<string name="live_stream_ended_tag">UKONČENO</string>
<string name="live_stream_planned_tag">PLÁNOVÁNO</string>
<string name="live_stream_is_offline">Živé vysílání je offline</string>
<string name="live_stream_has_ended">Živé vysílání ukončeno</string>
<string name="are_you_sure_you_want_to_log_out">Odhlášení vymaže všechny vaše místní informace. Ujistěte se, že máte zálohované své privátní klíče, abyste se vyhnuli ztrátě účtu. Chcete pokračovat?</string>
</resources>

View File

@@ -330,6 +330,8 @@ anz der Bedingungen ist erforderlich</string>
<string name="upload_server_nostrfilesdev">nostrfiles.dev - vertrauenswürdig</string>
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev kann die Datei ändern</string>
<string name="upload_server_nostrcheckme">nostrcheck.me - vertrauenswürdig</string>
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me kann die Datei ändern</string>
<string name="upload_server_imgur_nip94">Verifizierbares Imgur (NIP-94)</string>
<string name="upload_server_imgur_nip94_explainer">Überprüft, ob Imgur die Datei geändert hat. Neuer NIP: Andere Clients sehen es möglicherweise nicht</string>
@@ -343,6 +345,8 @@ anz der Bedingungen ist erforderlich</string>
<string name="upload_server_nostrfilesdev_nip94">Verifizierbares Nostrfiles.dev (NIP-94)</string>
<string name="upload_server_nostrfilesdev_nip94_explainer">Überprüft, ob Nostrfiles.dev die Datei geändert hat. Neuer NIP: Andere Clients sehen es möglicherweise nicht</string>
<string name="upload_server_nostrcheckme_nip94">Verifizierbares Nostrcheck.me (NIP-94)</string>
<string name="upload_server_nostrcheckme_nip94_explainer">Überprüft, ob Nostrcheck.me die Datei geändert hat. Neuer NIP: Andere Clients sehen es möglicherweise nicht</string>
<string name="upload_server_relays_nip95">Deine Relays (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">Dateien werden von deinen Relays gehostet. Neuer NIP: Überprüfe, ob sie unterstützt werden</string>
@@ -400,9 +404,51 @@ anz der Bedingungen ist erforderlich</string>
<string name="recommended_apps">Empfohlene Apps: </string>
<string name="filter_spam_from_strangers">Spam von Fremden filtern</string>
<string name="warn_when_posts_have_reports_from_your_follows">Warnung bei Meldungen von deinen Abonnements</string>
<string name="new_reaction_symbol">Neues Reaktionssymbol</string>
<string name="no_reaction_type_setup_long_press_to_change">Keine Reaktionstypen ausgewählt. Lange drücken, um zu ändern</string>
<string name="zapraiser">Zapraiser</string>
<string name="zapraiser_explainer">Fügt einen Zielbetrag in Sats für diesen Beitrag hinzu. Unterstützende Clients können dies als Fortschrittsbalken anzeigen, um Spenden zu fördern</string>
<string name="zapraiser_target_amount_in_sats">Zielbetrag in Sats</string>
<string name="sats_to_complete">Zapraiser bei %1$s. %2$s Sats bis zum Ziel</string>
<string name="read_from_relay">Von Relay lesen</string>
<string name="write_to_relay">In Relay schreiben</string>
<string name="an_error_ocurred_trying_to_get_relay_information">Ein Fehler ist beim Abrufen von Relay-Informationen von %1$s aufgetreten</string>
<string name="owner">Inhaber</string>
<string name="version">Version</string>
<string name="software">Software</string>
<string name="contact">Kontakt</string>
<string name="supports">Unterstützte NIPs</string>
<string name="admission_fees">Eintrittsgebühren</string>
<string name="payments_url">Zahlungs-URL</string>
<string name="limitations">Einschränkungen</string>
<string name="countries">Länder</string>
<string name="languages">Sprachen</string>
<string name="tags">Tags</string>
<string name="posting_policy">Veröffentlichungsrichtlinie</string>
<string name="message_length">Nachrichtenlänge</string>
<string name="subscriptions">Abonnements</string>
<string name="filters">Filter</string>
<string name="subscription_id_length">Länge der Abonnement-ID</string>
<string name="minimum_prefix">Mindestpräfix</string>
<string name="maximum_event_tags">Maximale Anzahl von Ereignis-Tags</string>
<string name="content_length">Inhaltslänge</string>
<string name="minimum_pow">Mindest-PoW</string>
<string name="auth">Authentifizierung</string>
<string name="payment">Zahlung</string>
<string name="cashu">Cashu-Token</string>
<string name="cashu_redeem">Einlösen</string>
<string name="live_stream_live_tag">LIVE</string>
<string name="live_stream_offline_tag">OFFLINE</string>
<string name="live_stream_ended_tag">BEENDET</string>
<string name="live_stream_planned_tag">GEPLANT</string>
<string name="live_stream_is_offline">Livestream ist offline</string>
<string name="live_stream_has_ended">Livestream beendet</string>
<string name="are_you_sure_you_want_to_log_out">Das Abmelden löscht alle Ihre lokalen Informationen. Stellen Sie sicher, dass Sie Ihre privaten Schlüssel gesichert haben, um einen Kontoverlust zu vermeiden. Möchten Sie fortfahren?</string>
</resources>

View File

@@ -391,7 +391,7 @@
<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>
<string name="recommended_apps">Ajánlottak: </string>
<string name="filter_spam_from_strangers">Idegenektől SPAM szűrése</string>
<string name="warn_when_posts_have_reports_from_your_follows">Figyelmeztessen ha a bejegyzések azok által akiket követek jelentve vannak</string>
@@ -406,4 +406,38 @@
<string name="sats_to_complete">A ZapGyűjtés %1$s-nál. %2$s sats kell a célig</string>
<string name="read_from_relay">Olvassás a csomópontból</string>
<string name="write_to_relay">Írás a csomópontra</string>
<string name="an_error_ocurred_trying_to_get_relay_information">Hiba történt a csomópont információ szerzés közben a %1$s -tól</string>
<string name="owner">Tulajdonos</string>
<string name="version">Verzió</string>
<string name="software">Szoftver</string>
<string name="contact">Kapcsolat</string>
<string name="supports">Támogatott NIP-ek</string>
<string name="admission_fees">Csatlakozás díja</string>
<string name="payments_url">Fizetési cím</string>
<string name="limitations">Korlátok</string>
<string name="countries">Országok</string>
<string name="languages">Nyelvek</string>
<string name="tags">Címkék</string>
<string name="posting_policy">Szabályzat</string>
<string name="message_length">Üzenet hossza</string>
<string name="subscriptions">Feliratkozások</string>
<string name="filters">Filterek</string>
<string name="subscription_id_length">Feliratkozás azonosítójának hossza</string>
<string name="minimum_prefix">Minimális előtag</string>
<string name="maximum_event_tags">Maximális eseménycímkék</string>
<string name="content_length">Tartalom hossza</string>
<string name="minimum_pow">Minimális PoW</string>
<string name="auth">Azonosítás</string>
<string name="payment">Fizetés</string>
<string name="cashu">Cashu Token</string>
<string name="cashu_redeem">Beváltás</string>
<string name="live_stream_live_tag">ÉLŐ</string>
<string name="live_stream_offline_tag">OFFLINE</string>
<string name="live_stream_ended_tag">VÉGE</string>
<string name="live_stream_planned_tag">ÜTEMEZETT</string>
<string name="live_stream_is_offline">Az élő közvetítés offline</string>
<string name="live_stream_has_ended">Az élő közvetítésnek vége</string>
<string name="are_you_sure_you_want_to_log_out">A Kijelentkezés minden helyben tárolt információt töröl. Bizonyosodj meg róla, hogy a privát kulcsod le van mentve, ezzel elkerülve a fiókod elvesztését. Folytatni akarod?</string>
</resources>

View File

@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.Amethyst" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">@color/purple_700</item>
<item name="android:windowBackground">@color/black</item>
<item name="android:windowLayoutInDisplayCutoutMode" tools:ignore="NewApi">shortEdges</item>\
</style>
</resources>

View File

@@ -344,9 +344,106 @@
<string name="upload_server_relays_nip95">Uw relays (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">Bestanden worden geüpload naar en gehost door relays. Ze zijn vrij van een vaste url (afhankelijkheid van derden). Zorg ervoor dat u een NIP-95 relay in uw lijst met relays hebt.</string>
<string name="connect_via_tor_short">Orbot/Tor setup</string>
<string name="connect_via_tor">Verbinden via Orbot setup</string>
<string name="do_you_really_want_to_disable_tor_title">Verbinding Orbot/Tor verbreken?</string>
<string name="do_you_really_want_to_disable_tor_text">Je gegevens worden onmiddellijk overgezet via het reguliere netwerk</string>
<string name="yes">Ja</string>
<string name="no">Nee</string>
<string name="follow_list_selection">Volgerslijst</string>
<string name="follow_list_kind3follows">Alle gevolgden</string>
<string name="follow_list_kind3follows">Iedereen</string>
<string name="follow_list_global">Globaal</string>
<string name="connect_through_your_orbot_setup_markdown">
## Verbind via Tor met Orbot
\n\n1. Installeer [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
\n2. Start Orbot
\n3. Controleer in Orbot de Socks-poort. Standaard wordt 9050 gebruikt
\n4. Wijzig indien nodig de poort in Orbot
\n5. Configureer de Socks-poort in dit scherm
\n6. Druk op de knop Activeren om Orbot als proxy te gebruiken
</string>
<string name="orbot_socks_port">Orbot Socks Poort</string>
<string name="invalid_port_number">Ongeldige poort</string>
<string name="use_orbot">Gebruik Orbot</string>
<string name="disconnect_from_your_orbot_setup">Ontkoppel Orbot/Tor</string>
<string name="app_notification_channel_id" translatable="false">StandaardKanaalID</string>
<string name="app_notification_private_message" translatable="false">Nieuwe notificatie ontvangen</string>
<string name="app_notification_dms_channel_id" translatable="false">PrivéberichtenID</string>
<string name="app_notification_dms_channel_name">Privéberichten</string>
<string name="app_notification_dms_channel_description">Geeft een melding wanneer er een privébericht binnenkomt</string>
<string name="app_notification_zaps_channel_id" translatable="false">ZapsID</string>
<string name="app_notification_zaps_channel_name">Zaps ontvangen</string>
<string name="app_notification_zaps_channel_description">Geeft een melding als iemand je zapt</string>
<string name="app_notification_zaps_channel_message">%1$s sats</string>
<string name="app_notification_zaps_channel_message_from">Van %1$s</string>
<string name="app_notification_zaps_channel_message_for">voor %1$s</string>
<string name="reply_notify">Meld: </string>
<string name="channel_list_join_conversation">Doe mee aan het gesprek</string>
<string name="channel_list_user_or_group_id">Gebruiker of groeps ID</string>
<string name="channel_list_user_or_group_id_demo">npub, nevent of hex</string>
<string name="channel_list_create_channel">Maken</string>
<string name="channel_list_join_channel">Deelnemen</string>
<string name="today">Vandaag</string>
<string name="content_warning">Content waarschuwing</string>
<string name="content_warning_explanation">Dit bericht bevat gevoelige inhoud die sommige mensen beledigend of verontrustend kunnen vinden</string>
<string name="content_warning_hide_all_sensitive_content">Verberg altijd gevoelige inhoud</string>
<string name="content_warning_show_all_sensitive_content">Toon altijd gevoelige inhoud</string>
<string name="content_warning_see_warnings">Toon altijd content waarshuwingen</string>
<string name="recommended_apps">Raadt aan: </string>
<string name="filter_spam_from_strangers">Filter spam van onbekenden</string>
<string name="warn_when_posts_have_reports_from_your_follows">Waarschuw wanneer berichten meldingen hebben van je volgers</string>
<string name="new_reaction_symbol">Nieuwe reactie symbool</string>
<string name="no_reaction_type_setup_long_press_to_change">Geen reactietypes geselecteerd. Lang indrukken om te wijzigen</string>
<string name="zapraiser">Zapraiser</string>
<string name="zapraiser_explainer">Voegt een streefaantal sats toe dat moet worden opgehaald voor deze post. Ondersteunende clients kunnen dit als een voortgangsbalk weergeven om donaties te stimuleren.</string>
<string name="zapraiser_target_amount_in_sats">Streefaantal in sats</string>
<string name="sats_to_complete">Zapraiser op %1$s. %2$s sats tot doel</string>
<string name="read_from_relay">Lees van relay</string>
<string name="write_to_relay">Schrijf naar relay</string>
<string name="an_error_ocurred_trying_to_get_relay_information">Er is een fout opgetreden bij het ophalen van de relay-informatie van %1$s</string>
<string name="owner">Eigenaar</string>
<string name="version">Versie</string>
<string name="software">Software</string>
<string name="contact">Contact</string>
<string name="supports">Ondersteunde NIPs</string>
<string name="admission_fees">Toelatingskosten</string>
<string name="payments_url">Betaallink</string>
<string name="limitations">Beperkingen</string>
<string name="countries">Landen</string>
<string name="languages">Talen</string>
<string name="tags">Tags</string>
<string name="posting_policy">Postbeleid</string>
<string name="message_length">Lengte van bericht</string>
<string name="subscriptions">Abonnementen</string>
<string name="filters">Filters</string>
<string name="subscription_id_length">Lengte abonnement-id</string>
<string name="minimum_prefix">Minimum prefix</string>
<string name="maximum_event_tags">Maximum event tags</string>
<string name="content_length">Content lengte</string>
<string name="minimum_pow">Minimum PoW</string>
<string name="auth">Auth</string>
<string name="payment">Betaling</string>
<string name="live_stream_live_tag">LIVE</string>
<string name="live_stream_offline_tag">OFFLINE</string>
<string name="live_stream_ended_tag">GEËINDIGD</string>
<string name="live_stream_planned_tag">INGEPLAND</string>
<string name="live_stream_is_offline">Livestream is offline</string>
<string name="live_stream_has_ended">Livestream is geëindigd</string>
</resources>

View File

@@ -323,6 +323,8 @@
<string name="upload_server_nostrfilesdev">nostrfiles.dev - betrodd</string>
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev kan ändra filen</string>
<string name="upload_server_nostrcheckme">nostrcheck.me - betrodd</string>
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me kan ändra filen</string>
<string name="upload_server_imgur_nip94">Verifierbar Imgur (NIP-94)</string>
<string name="upload_server_imgur_nip94_explainer">Kontrollerar om Imgur modifierat filen. Nytt NIP: andra klienter kanske inte ser det</string>
@@ -336,6 +338,8 @@
<string name="upload_server_nostrfilesdev_nip94">Verifierbar Nostrfiles.dev (NIP-94)</string>
<string name="upload_server_nostrfilesdev_nip94_explainer">Kontrollerar om Nostrfiles.dev modifierat filen. Nytt NIP: andra klienter kanske inte ser det</string>
<string name="upload_server_nostrcheckme_nip94">Verifierbar Nostrcheck.me (NIP-94)</string>
<string name="upload_server_nostrcheckme_nip94_explainer">Kontrollerar om Nostrcheck.me modifierat filen. Nytt NIP: andra klienter kanske inte ser det</string>
<string name="upload_server_relays_nip95">Dina Reläer (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">Filer är värd för dina reläer. Nytt NIP: kontrollera om de stöder</string>
@@ -387,6 +391,54 @@
<string name="content_warning_hide_all_sensitive_content">Dölj alltid känsligt innehåll</string>
<string name="content_warning_show_all_sensitive_content">Visa alltid känsligt innehåll</string>
<string name="content_warning_see_warnings">Visa alltid innehållsvarningar</string>
<string name="recommended_apps">Rekommenderas: </string>
<string name="filter_spam_from_strangers">Filtrera skräppost från främlingar</string>
<string name="warn_when_posts_have_reports_from_your_follows">Varna när inlägg har rapporter från dina följare</string>
<string name="new_reaction_symbol">Ny reaktionssymbol</string>
<string name="no_reaction_type_setup_long_press_to_change">Inga reaktionstyper valda. Håll ned för att ändra</string>
<string name="zapraiser">Zapraiser</string>
<string name="zapraiser_explainer">Lägger till en målsumma av sats för detta inlägg. Stödjande klienter kan visa detta som en framstegsindikator för att stimulera donationer</string>
<string name="zapraiser_target_amount_in_sats">Målsumma i sats</string>
<string name="sats_to_complete">Zapraiser på %1$s. %2$s sats kvar till målet</string>
<string name="read_from_relay">Läs från Relay</string>
<string name="write_to_relay">Skriv till Relay</string>
<string name="an_error_ocurred_trying_to_get_relay_information">Ett fel inträffade vid försök att hämta information från Relay %1$s</string>
<string name="owner">Ägare</string>
<string name="version">Version</string>
<string name="software">Programvara</string>
<string name="contact">Kontakt</string>
<string name="supports">Stödda NIPs</string>
<string name="admission_fees">Entréavgifter</string>
<string name="payments_url">Betalnings-URL</string>
<string name="limitations">Begränsningar</string>
<string name="countries">Länder</string>
<string name="languages">Språk</string>
<string name="tags">Taggar</string>
<string name="posting_policy">Publiceringspolicy</string>
<string name="message_length">Meddelandelängd</string>
<string name="subscriptions">Prenumerationer</string>
<string name="filters">Filter</string>
<string name="subscription_id_length">Längd på prenumerations-ID</string>
<string name="minimum_prefix">Minsta prefix</string>
<string name="maximum_event_tags">Maximalt antal händelse-taggar</string>
<string name="content_length">Innehållslängd</string>
<string name="minimum_pow">Minsta PoW</string>
<string name="auth">Autentisering</string>
<string name="payment">Betalning</string>
<string name="cashu">Cashu Token</string>
<string name="cashu_redeem">Inlösen</string>
<string name="live_stream_live_tag">LIVE</string>
<string name="live_stream_offline_tag">OFFLINE</string>
<string name="live_stream_ended_tag">SLUTADE</string>
<string name="live_stream_planned_tag">PLANERAD</string>
<string name="live_stream_is_offline">Direktsändning är offline</string>
<string name="live_stream_has_ended">Direktsändning avslutad</string>
<string name="are_you_sure_you_want_to_log_out">Att logga ut raderar all din lokala information. Se till att ha dina privata nycklar säkerhetskopierade för att undvika att förlora ditt konto. Vill du fortsätta?</string>
</resources>

View File

@@ -456,6 +456,8 @@
<string name="minimum_pow">Minimum PoW</string>
<string name="auth">Auth</string>
<string name="payment">Payment</string>
<string name="cashu">Cashu Token</string>
<string name="cashu_redeem">Redeem</string>
<string name="live_stream_live_tag">LIVE</string>
<string name="live_stream_offline_tag">OFFLINE</string>
@@ -464,4 +466,6 @@
<string name="live_stream_is_offline">Livestream is Offline</string>
<string name="live_stream_has_ended">Livestream Ended</string>
<string name="are_you_sure_you_want_to_log_out">Logging out deletes all your local information. Make sure to have your private keys backed up to avoid losing your account. Do you want to continue?</string>
<string name="followed_tags">Followed Tags</string>
</resources>

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.Amethyst" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">@color/purple_700</item>
<item name="android:windowLayoutInDisplayCutoutMode" tools:ignore="NewApi">shortEdges</item>
</style>
</resources>

View File

@@ -12,6 +12,8 @@ import com.google.mlkit.nl.translate.Translator
import com.google.mlkit.nl.translate.TranslatorOptions
import com.linkedin.urls.detection.UrlDetector
import com.linkedin.urls.detection.UrlDetectorOptions
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import java.util.concurrent.Executors
import java.util.regex.Pattern
@Immutable
@@ -22,7 +24,9 @@ data class ResultOrError(
)
object LanguageTranslatorService {
private val options = LanguageIdentificationOptions.Builder().setConfidenceThreshold(0.6f).build()
var executorService = Executors.newScheduledThreadPool(5)
private val options = LanguageIdentificationOptions.Builder().setExecutor(executorService).setConfidenceThreshold(0.6f).build()
private val languageIdentification = LanguageIdentification.getClient(options)
val lnRegex = Pattern.compile("\\blnbc[a-z0-9]+\\b", Pattern.CASE_INSENSITIVE)
val tagRegex = Pattern.compile("(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)", Pattern.CASE_INSENSITIVE)
@@ -48,6 +52,7 @@ object LanguageTranslatorService {
}
fun translate(text: String, source: String, target: String): Task<ResultOrError> {
checkNotInMainThread()
val sourceLangCode = TranslateLanguage.fromLanguageTag(source)
val targetLangCode = TranslateLanguage.fromLanguageTag(target)
@@ -62,19 +67,22 @@ object LanguageTranslatorService {
val translator = translators[options]
return translator.downloadModelIfNeeded().onSuccessTask {
val tasks = mutableListOf<Task<String>>()
return translator.downloadModelIfNeeded().onSuccessTask(executorService) {
checkNotInMainThread()
val tasks = mutableListOf<Task<String>>()
val dict = lnDictionary(text) + urlDictionary(text) + tagDictionary(text)
for (paragraph in encodeDictionary(text, dict).split("\n")) {
tasks.add(translator.translate(paragraph))
}
Tasks.whenAll(tasks).continueWith {
Tasks.whenAll(tasks).continueWith(executorService) {
checkNotInMainThread()
val results: MutableList<String> = ArrayList()
for (task in tasks) {
var fixedText = task.result.replace("# [", "#[") // fixes tags that always return with a space
val fixedText = task.result.replace("# [", "#[") // fixes tags that always return with a space
results.add(decodeDictionary(fixedText, dict))
}
ResultOrError(results.joinToString("\n"), source, target)
@@ -140,7 +148,7 @@ object LanguageTranslatorService {
}
fun autoTranslate(text: String, dontTranslateFrom: Set<String>, translateTo: String): Task<ResultOrError> {
return identifyLanguage(text).onSuccessTask {
return identifyLanguage(text).onSuccessTask(executorService) {
if (it == translateTo) {
Tasks.forCanceled()
} else if (it != "und" && !dontTranslateFrom.contains(it)) {

View File

@@ -1,6 +1,7 @@
package com.vitorpamplona.amethyst.ui.components
import android.content.res.Resources
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.Divider
@@ -13,6 +14,7 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
@@ -33,7 +35,6 @@ import androidx.compose.ui.unit.dp
import androidx.core.os.ConfigurationCompat
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
import com.vitorpamplona.amethyst.service.lang.ResultOrError
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
@@ -41,6 +42,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Locale
@Immutable
data class TranslationConfig(
val result: String?,
val sourceLang: String?,
val targetLang: String?,
val showOriginal: Boolean
)
@Composable
fun TranslatableRichTextViewer(
content: String,
@@ -51,32 +60,53 @@ fun TranslatableRichTextViewer(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var translatedTextState by remember {
mutableStateOf(TranslationConfig(content, null, null, false))
}
TranslateAndWatchLanguageChanges(content, accountViewModel) { result ->
if (!translatedTextState.result.equals(result.result, true) ||
translatedTextState.sourceLang != result.sourceLang ||
translatedTextState.targetLang != result.targetLang
) {
translatedTextState = result
}
}
Crossfade(targetState = translatedTextState) {
RenderText(
it,
content,
canPreview,
modifier,
tags,
backgroundColor,
accountViewModel,
nav
)
}
}
@Composable
private fun RenderText(
translatedTextState: TranslationConfig,
content: String,
canPreview: Boolean,
modifier: Modifier,
tags: ImmutableListOfLists<String>,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var showOriginal by remember(translatedTextState) { mutableStateOf(translatedTextState.showOriginal) }
val toBeViewed by remember(translatedTextState) {
derivedStateOf {
if (showOriginal) content else translatedTextState.result ?: content
}
}
Column {
var translatedTextState by remember {
mutableStateOf(ResultOrError(content, null, null))
}
var showOriginal by remember { mutableStateOf(false) }
TranslateAndWatchLanguageChanges(content, accountViewModel) { result, newShowOriginal ->
if (translatedTextState.result != result.result ||
translatedTextState.sourceLang != result.sourceLang ||
translatedTextState.targetLang != result.targetLang
) {
translatedTextState = result
}
if (showOriginal != newShowOriginal) {
showOriginal = newShowOriginal
}
}
val toBeViewed by remember(translatedTextState) {
derivedStateOf {
if (showOriginal) content else translatedTextState.result ?: content
}
}
ExpandableRichTextViewer(
toBeViewed,
canPreview,
@@ -92,8 +122,8 @@ fun TranslatableRichTextViewer(
translatedTextState.sourceLang != translatedTextState.targetLang
) {
TranslationMessage(
translatedTextState.sourceLang!!,
translatedTextState.targetLang!!,
translatedTextState.sourceLang,
translatedTextState.targetLang,
accountViewModel
) {
showOriginal = it
@@ -280,23 +310,30 @@ private fun TranslationMessage(
}
@Composable
fun TranslateAndWatchLanguageChanges(content: String, accountViewModel: AccountViewModel, onTranslated: (ResultOrError, Boolean) -> Unit) {
fun TranslateAndWatchLanguageChanges(content: String, accountViewModel: AccountViewModel, onTranslated: (TranslationConfig) -> Unit) {
val accountState by accountViewModel.accountLanguagesLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val scope = rememberCoroutineScope()
LaunchedEffect(accountState) {
scope.launch(Dispatchers.IO) {
launch(Dispatchers.IO) {
LanguageTranslatorService.autoTranslate(
content,
account.dontTranslateFrom,
account.translateTo
).addOnCompleteListener { task ->
if (task.isSuccessful && content != task.result.result) {
if (task.isSuccessful && !content.equals(task.result.result, true)) {
if (task.result.sourceLang != null && task.result.targetLang != null) {
val preference = account.preferenceBetween(task.result.sourceLang!!, task.result.targetLang!!)
onTranslated(task.result, preference == task.result.sourceLang)
val newConfig = TranslationConfig(
result = task.result.result,
sourceLang = task.result.sourceLang,
targetLang = task.result.targetLang,
showOriginal = preference == task.result.sourceLang
)
// withContext(Dispatchers.Main) {
onTranslated(newConfig)
// }
}
}
}

View File

@@ -1,8 +1,11 @@
package com.vitorpamplona.amethyst.service
import android.os.Looper
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.SpyK
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import org.junit.After
import org.junit.Assert.assertEquals
@@ -19,7 +22,12 @@ class Nip05VerifierTest {
var nip05Verifier = Nip05Verifier()
@Before
fun setUp() = MockKAnnotations.init(this)
fun setUp() {
mockkStatic(Looper::class)
every { Looper.myLooper() } returns mockk<Looper>()
every { Looper.getMainLooper() } returns mockk<Looper>()
MockKAnnotations.init(this)
}
@Test
fun `test with matching case on user name`() {

View File

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 170.16 186.2"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:url(#linear-gradient-2);}</style><linearGradient id="linear-gradient" x1="80.19" y1="57.17" x2="91.66" y2="57.17" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#652d80"/><stop offset="1" stop-color="#2598cf"/></linearGradient><linearGradient id="linear-gradient-2" x1="335.87" y1="297.64" x2="506.02" y2="297.64" xlink:href="#linear-gradient"/></defs><title>Amethyst_svg_monogram</title><ellipse class="cls-1" cx="85.93" cy="57.17" rx="5.74" ry="5.64"/><path class="cls-2" d="M506,347.08c-.2-.61-46.85-111.69-59.68-142.5-15.62,0-48.61,0-71.88,0-6.83,16.89-24.46,59.94-38.6,94.62,7.75,18.49,14.71,35.1,20.08,47.91l29.27,0a2,2,0,0,0,1.87-2.35c-.25-5.91-16.34-27.39-11.54-50.63,1.28-5,2.26-10.08,3.66-15.06a175.77,175.77,0,0,1,13.44-34.8c.89-1.69,1.3-2.45,3.35-1.17,4.37,2.72,9.25,2.56,14.11,2.21,4.19-.91,6.23-2.92,13.91-.88,1.52,0,3,0,4.55.14,3.69.29,7.15,1.17,9.37,4.51,2.42,3.65,2.81,7.78,2.42,12-.59,6.31-.17,12.19,5.17,16.64a57.52,57.52,0,0,0,6,4c2.65,1.7,5.85,2.44,8.12,4.8,1.34,1.39,2.13,2.87,1.55,4.85s-2.13,2.91-4.17,3.13c-5.16.56-10.2-.49-15.27-1.1-.66-.08-1.31-.13-2-.17a11.47,11.47,0,0,1-3.81.13l-1.19,0a26.7,26.7,0,0,0-5.9,1.41c-4.78,1.74-9.13,3.66-14.77,3.56a4.32,4.32,0,0,0-2.05.89c-4.42,3.93-7.08,8.89-4.87,16.14,6.06,16.93,21.61,57.77,28.29,75.4-.19-11.94-.24-33.32-.28-43.7C461.83,347.1,502.74,347.08,506,347.08Z" transform="translate(-335.87 -204.54)"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 424.96 310.43"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:url(#linear-gradient-2);}.cls-3{fill:#672f80;}</style><linearGradient id="linear-gradient" x1="207.59" y1="57.17" x2="219.06" y2="57.17" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#652d80"/><stop offset="1" stop-color="#2598cf"/></linearGradient><linearGradient id="linear-gradient-2" x1="335.87" y1="235.52" x2="506.02" y2="235.52" xlink:href="#linear-gradient"/></defs><title>Amethyst_svg_logo</title><ellipse class="cls-1" cx="213.33" cy="57.17" rx="5.74" ry="5.64"/><path class="cls-2" d="M506,285c-.2-.61-46.85-111.69-59.68-142.5-15.62,0-48.61,0-71.88,0-6.83,16.89-24.46,59.94-38.6,94.62,7.75,18.49,14.71,35.1,20.08,47.91l29.27,0a2,2,0,0,0,1.87-2.35c-.25-5.91-16.34-27.39-11.54-50.63,1.28-5,2.26-10.08,3.66-15.06a175.77,175.77,0,0,1,13.44-34.8c.89-1.69,1.3-2.45,3.35-1.17,4.37,2.72,9.25,2.56,14.11,2.21,4.19-.91,6.23-2.92,13.91-.88,1.52,0,3,0,4.55.14,3.69.29,7.15,1.17,9.37,4.51,2.42,3.65,2.81,7.78,2.42,12-.59,6.31-.17,12.19,5.17,16.64a57.52,57.52,0,0,0,6,4c2.65,1.7,5.85,2.44,8.12,4.8,1.34,1.39,2.13,2.87,1.55,4.85S459,232.1,457,232.32c-5.16.56-10.2-.49-15.27-1.1-.66-.08-1.31-.13-2-.17a11.47,11.47,0,0,1-3.81.13l-1.19,0a26.7,26.7,0,0,0-5.9,1.41c-4.78,1.74-9.13,3.66-14.77,3.56a4.32,4.32,0,0,0-2.05.89c-4.42,3.93-7.08,8.89-4.87,16.14,6.06,16.93,21.61,57.77,28.29,75.4-.19-11.94-.24-33.32-.28-43.7C461.83,285,502.74,285,506,285Z" transform="translate(-208.47 -142.42)"/><path class="cls-3" d="M255.51,415.12H226.57l-5.86,17.17H208.47l27.74-76.8h9.68l27.74,76.8H261.43Zm-3.3-10.32-11-33.55H241l-11,33.55Z" transform="translate(-208.47 -142.42)"/><path class="cls-3" d="M284.13,377.28h10.94v6.45h.21a17.59,17.59,0,0,1,14.18-6.44,20.28,20.28,0,0,1,9.11,1.94,27.2,27.2,0,0,1,6.23,5.18q6.38-7.22,17.11-7.12A18.64,18.64,0,0,1,354.25,382q5.55,4.82,5.76,14.13v36.11H349.07V399.95q0-5.7-3-8.69a10.32,10.32,0,0,0-7.69-3,10.49,10.49,0,0,0-7.75,3q-3.09,3-3.09,8.69v32.34H316.63V399.95q0-5.7-3-8.69a10.49,10.49,0,0,0-7.75-3,10.32,10.32,0,0,0-7.69,3q-3.14,3-3.14,8.69v32.34H284.13Z" transform="translate(-208.47 -142.42)"/><path class="cls-3" d="M580.19,422.18a14.63,14.63,0,0,0,7.69-1.81,5.65,5.65,0,0,0,3-5.07q0-2.9-1.78-4a9.43,9.43,0,0,0-4.87-1.29l-9.63-.83a18.43,18.43,0,0,1-11.15-4.56q-4.4-3.83-4.5-10.72.1-8.28,5.91-12.43,5.65-4.14,14.34-4.14a44,44,0,0,1,6.75.47,34.06,34.06,0,0,1,5.6,1.45,32,32,0,0,1,9,5.44l-6.75,7.82a33.81,33.81,0,0,0-6.65-3.68,22.6,22.6,0,0,0-8-1.29q-5.13,0-7.17,1.81a5.82,5.82,0,0,0-2.15,4.66,3.64,3.64,0,0,0,1.62,3.16q1.57,1.5,5.55,1.92l9,.67q8.48.62,12.25,5.07a17,17,0,0,1,3.51,11,13.92,13.92,0,0,1-6.44,12.06q-6.28,4.35-16.07,4.45-13.29,0-23.92-9.63l7.64-7.4Q569.67,422.18,580.19,422.18Z" transform="translate(-208.47 -142.42)"/><path class="cls-3" d="M382.11,408.91q0,6.46,3.79,9.81a12.75,12.75,0,0,0,9.26,3.41,16.52,16.52,0,0,0,11.94-5.22l7.94,6.66q-8.36,8.78-19.67,8.68A28.93,28.93,0,0,1,387,431a20.62,20.62,0,0,1-4-1.81,19.33,19.33,0,0,1-3.84-2.43,21.24,21.24,0,0,1-5.63-8.37q-2.37-5.16-2.37-13.43a36.74,36.74,0,0,1,2.05-13.22,23,23,0,0,1,5.26-8.47,19.66,19.66,0,0,1,7.31-4.55,23.83,23.83,0,0,1,7.89-1.39,22.94,22.94,0,0,1,15.83,6q6.57,6,6.68,16.63v8.88Zm23-8.88q-.21-6.25-3.42-9.45a12.28,12.28,0,0,0-16.2,0q-3.1,3.2-3.42,9.45Z" transform="translate(-208.47 -142.42)"/><path class="cls-3" d="M428.28,360.59h11v16.73h8v8.36h-8V417.9q-.11,4.15,3.89,4h4.1v10.36h-5.73a13,13,0,0,1-9.47-3.58q-3.79-3.42-3.79-9.89V385.68h-5.47v-8.36h5.47Z" transform="translate(-208.47 -142.42)"/><path class="cls-3" d="M614.44,360.59h11v16.73h8v8.36h-8V417.9q-.11,4.15,3.89,4h4.1v10.36h-5.73a13,13,0,0,1-9.47-3.58q-3.79-3.42-3.79-9.89V385.68H609v-8.36h5.47Z" transform="translate(-208.47 -142.42)"/><path class="cls-3" d="M457.57,355.72h11v27.75h.21A17.62,17.62,0,0,1,483,377a18.73,18.73,0,0,1,12.41,4.79q5.57,4.84,5.84,14.2v36.28h-11v-32.5q0-5.73-3.05-8.73a10.37,10.37,0,0,0-7.73-3,10.54,10.54,0,0,0-7.78,3q-3.1,3-3.1,8.73v32.5h-11Z" transform="translate(-208.47 -142.42)"/><path class="cls-3" d="M506.68,377.29H518.3l12.94,38.23h.21l12.88-38.23H556L532.5,442.08q-2.26,6.15-6.26,8.52a18.45,18.45,0,0,1-9.15,2.26h-3.63V442.5h3.21a7.38,7.38,0,0,0,4-1.16,6.37,6.37,0,0,0,2.58-3.58l3-8Z" transform="translate(-208.47 -142.42)"/></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 424.96 310.43"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:url(#linear-gradient-2);}.cls-3{fill:#672f80;}</style><linearGradient id="linear-gradient" x1="207.59" y1="57.17" x2="219.06" y2="57.17" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#652d80"/><stop offset="1" stop-color="#2598cf"/></linearGradient><linearGradient id="linear-gradient-2" x1="335.87" y1="235.52" x2="506.02" y2="235.52" xlink:href="#linear-gradient"/></defs><title>Amethyst_svg_logo</title><ellipse class="cls-1" cx="213.33" cy="57.17" rx="5.74" ry="5.64"/><path class="cls-2" d="M506,285c-.2-.61-46.85-111.69-59.68-142.5-15.62,0-48.61,0-71.88,0-6.83,16.89-24.46,59.94-38.6,94.62,7.75,18.49,14.71,35.1,20.08,47.91l29.27,0a2,2,0,0,0,1.87-2.35c-.25-5.91-16.34-27.39-11.54-50.63,1.28-5,2.26-10.08,3.66-15.06a175.77,175.77,0,0,1,13.44-34.8c.89-1.69,1.3-2.45,3.35-1.17,4.37,2.72,9.25,2.56,14.11,2.21,4.19-.91,6.23-2.92,13.91-.88,1.52,0,3,0,4.55.14,3.69.29,7.15,1.17,9.37,4.51,2.42,3.65,2.81,7.78,2.42,12-.59,6.31-.17,12.19,5.17,16.64a57.52,57.52,0,0,0,6,4c2.65,1.7,5.85,2.44,8.12,4.8,1.34,1.39,2.13,2.87,1.55,4.85S459,232.1,457,232.32c-5.16.56-10.2-.49-15.27-1.1-.66-.08-1.31-.13-2-.17a11.47,11.47,0,0,1-3.81.13l-1.19,0a26.7,26.7,0,0,0-5.9,1.41c-4.78,1.74-9.13,3.66-14.77,3.56a4.32,4.32,0,0,0-2.05.89c-4.42,3.93-7.08,8.89-4.87,16.14,6.06,16.93,21.61,57.77,28.29,75.4-.19-11.94-.24-33.32-.28-43.7C461.83,285,502.74,285,506,285Z" transform="translate(-208.47 -142.42)"/><path class="cls-3" d="M255.51,415.12H226.57l-5.86,17.17H208.47l27.74-76.8h9.68l27.74,76.8H261.43Zm-3.3-10.32-11-33.55H241l-11,33.55Z" transform="translate(-208.47 -142.42)"/><path class="cls-3" d="M284.13,377.28h10.94v6.45h.21a17.59,17.59,0,0,1,14.18-6.44,20.28,20.28,0,0,1,9.11,1.94,27.2,27.2,0,0,1,6.23,5.18q6.38-7.22,17.11-7.12A18.64,18.64,0,0,1,354.25,382q5.55,4.82,5.76,14.13v36.11H349.07V399.95q0-5.7-3-8.69a10.32,10.32,0,0,0-7.69-3,10.49,10.49,0,0,0-7.75,3q-3.09,3-3.09,8.69v32.34H316.63V399.95q0-5.7-3-8.69a10.49,10.49,0,0,0-7.75-3,10.32,10.32,0,0,0-7.69,3q-3.14,3-3.14,8.69v32.34H284.13Z" transform="translate(-208.47 -142.42)"/><path class="cls-3" d="M580.19,422.18a14.63,14.63,0,0,0,7.69-1.81,5.65,5.65,0,0,0,3-5.07q0-2.9-1.78-4a9.43,9.43,0,0,0-4.87-1.29l-9.63-.83a18.43,18.43,0,0,1-11.15-4.56q-4.4-3.83-4.5-10.72.1-8.28,5.91-12.43,5.65-4.14,14.34-4.14a44,44,0,0,1,6.75.47,34.06,34.06,0,0,1,5.6,1.45,32,32,0,0,1,9,5.44l-6.75,7.82a33.81,33.81,0,0,0-6.65-3.68,22.6,22.6,0,0,0-8-1.29q-5.13,0-7.17,1.81a5.82,5.82,0,0,0-2.15,4.66,3.64,3.64,0,0,0,1.62,3.16q1.57,1.5,5.55,1.92l9,.67q8.48.62,12.25,5.07a17,17,0,0,1,3.51,11,13.92,13.92,0,0,1-6.44,12.06q-6.28,4.35-16.07,4.45-13.29,0-23.92-9.63l7.64-7.4Q569.67,422.18,580.19,422.18Z" transform="translate(-208.47 -142.42)"/><path class="cls-3" d="M382.11,408.91q0,6.46,3.79,9.81a12.75,12.75,0,0,0,9.26,3.41,16.52,16.52,0,0,0,11.94-5.22l7.94,6.66q-8.36,8.78-19.67,8.68A28.93,28.93,0,0,1,387,431a20.62,20.62,0,0,1-4-1.81,19.33,19.33,0,0,1-3.84-2.43,21.24,21.24,0,0,1-5.63-8.37q-2.37-5.16-2.37-13.43a36.74,36.74,0,0,1,2.05-13.22,23,23,0,0,1,5.26-8.47,19.66,19.66,0,0,1,7.31-4.55,23.83,23.83,0,0,1,7.89-1.39,22.94,22.94,0,0,1,15.83,6q6.57,6,6.68,16.63v8.88Zm23-8.88q-.21-6.25-3.42-9.45a12.28,12.28,0,0,0-16.2,0q-3.1,3.2-3.42,9.45Z" transform="translate(-208.47 -142.42)"/><path class="cls-3" d="M428.28,360.59h11v16.73h8v8.36h-8V417.9q-.11,4.15,3.89,4h4.1v10.36h-5.73a13,13,0,0,1-9.47-3.58q-3.79-3.42-3.79-9.89V385.68h-5.47v-8.36h5.47Z" transform="translate(-208.47 -142.42)"/><path class="cls-3" d="M614.44,360.59h11v16.73h8v8.36h-8V417.9q-.11,4.15,3.89,4h4.1v10.36h-5.73a13,13,0,0,1-9.47-3.58q-3.79-3.42-3.79-9.89V385.68H609v-8.36h5.47Z" transform="translate(-208.47 -142.42)"/><path class="cls-3" d="M457.57,355.72h11v27.75h.21A17.62,17.62,0,0,1,483,377a18.73,18.73,0,0,1,12.41,4.79q5.57,4.84,5.84,14.2v36.28h-11v-32.5q0-5.73-3.05-8.73a10.37,10.37,0,0,0-7.73-3,10.54,10.54,0,0,0-7.78,3q-3.1,3-3.1,8.73v32.5h-11Z" transform="translate(-208.47 -142.42)"/><path class="cls-3" d="M506.68,377.29H518.3l12.94,38.23h.21l12.88-38.23H556L532.5,442.08q-2.26,6.15-6.26,8.52a18.45,18.45,0,0,1-9.15,2.26h-3.63V442.5h3.21a7.38,7.38,0,0,0,4-1.16,6.37,6.37,0,0,0,2.58-3.58l3-8Z" transform="translate(-208.47 -142.42)"/></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More