Compare commits

...

51 Commits

Author SHA1 Message Date
Vitor Pamplona
ad5a4d0b59 v0.58.3 2023-06-15 18:51:39 -04:00
Vitor Pamplona
3116ea1ad9 Fixes reaction sizes and alignments. 2023-06-15 12:21:44 -04:00
Vitor Pamplona
72e474c9ec Bug fix for when deleting the reaction event reverting to the old like heart. 2023-06-15 09:37:09 -04:00
Vitor Pamplona
fe9e85c282 v0.58.2 2023-06-14 20:58:51 -04:00
Vitor Pamplona
01ac90fbec BugFix for active like all the time 2023-06-14 20:55:46 -04:00
Vitor Pamplona
dc079941e7 v0.58.1 2023-06-14 20:28:47 -04:00
Vitor Pamplona
9a7a94cb77 Cropping long reactions to two chars for emojis 2023-06-14 20:27:46 -04:00
Vitor Pamplona
a44703cb49 v0.58.0 2023-06-14 16:50:54 -04:00
Vitor Pamplona
52bd6b4443 Custom reactions 2023-06-14 16:49:51 -04:00
Vitor Pamplona
a9e1ce5aec Removing modifiers from remembers and into global singletons. 2023-06-14 14:36:10 -04:00
Vitor Pamplona
639fdb174d BugFix Add -> And 2023-06-14 14:32:55 -04:00
Vitor Pamplona
3cd02ee6cf Merge remote-tracking branch 'origin/HEAD' 2023-06-14 13:49:10 -04:00
Vitor Pamplona
52199fe281 Starts to use getXIfExists functions to preload objects 2023-06-14 13:48:58 -04:00
Vitor Pamplona
5616fa52d1 Simplifies URL Preview call 2023-06-14 13:48:35 -04:00
Vitor Pamplona
7241c8bd9f Merge pull request #454 from ZsZolee/main
Small Hungarian translation
2023-06-14 10:55:16 -04:00
Zoltan
991131f8cd Small Hungarian translation 2023-06-14 09:35:36 +02:00
Vitor Pamplona
a24c95070c v0.57.0 2023-06-13 17:16:20 -04:00
Vitor Pamplona
a346d31554 Finally adds markers to kind 1s 2023-06-13 17:08:50 -04:00
Vitor Pamplona
9b5c7ad23f - Moves zaps from a Map<Request,Response> to a List<CustomClass> to avoid recompositions due to the use of the unstable Pair object
- Creates the class ZapAmountCommentNotification to be used instead of the unstable Triple class.
2023-06-13 16:33:52 -04:00
Vitor Pamplona
a01a060abc Uses animateItemPlacement to show new Notes and update the size of current ones. 2023-06-13 16:31:23 -04:00
Vitor Pamplona
1cd39e0d8f Removes the use of by on the quick action menu's state because it was triggering recompositions 2023-06-13 16:30:37 -04:00
Vitor Pamplona
305ecc6a2e New update removes access to the backQueue 2023-06-13 16:29:21 -04:00
Vitor Pamplona
858f94ae74 Moves icon sizes in the bottom row to remember functions 2023-06-13 16:28:42 -04:00
Vitor Pamplona
fc224d99e3 Updates dependencies. 2023-06-13 10:52:29 -04:00
Vitor Pamplona
b8584964d1 Activates build cache 2023-06-13 10:47:08 -04:00
Vitor Pamplona
fbd0c96a8a Merge remote-tracking branch 'origin/HEAD' 2023-06-12 16:58:32 -04:00
Vitor Pamplona
e2251f9c86 Removing the minimum size for rows in the feed due to Hidden posts from the user's block list. 2023-06-12 16:58:20 -04:00
Vitor Pamplona
eaaacbf218 Turns NIP-05 verification symbol into a State variable to help correctly rendering updates. 2023-06-12 16:56:24 -04:00
Vitor Pamplona
243fbeed44 Remembers some variables on NoteCompose 2023-06-12 16:55:51 -04:00
Vitor Pamplona
d62bab1b08 Makes relay badge list in chatroom Immutable 2023-06-12 16:55:27 -04:00
Vitor Pamplona
70e068ed17 Breaks down user renderer compositions 2023-06-12 16:55:08 -04:00
Vitor Pamplona
824e8795d5 Formatting 2023-06-12 16:54:45 -04:00
Vitor Pamplona
f768a09bd8 Removes unused variable from Translation results 2023-06-12 16:54:15 -04:00
Vitor Pamplona
1a69a5a4c7 Merge pull request #449 from greenart7c3/main
add option to opt-out from automatic spam and report filters
2023-06-12 16:37:03 -04:00
Vitor Pamplona
b9cc3781bb Merge pull request #451 from licaon-kter/patch-1
Typo fix
2023-06-12 11:10:22 -04:00
Licaon_Kter
97964ed65c Typo fix 2023-06-12 15:04:25 +00:00
Vitor Pamplona
42a1d157bf Fixes draft note in privacy.md 2023-06-12 10:25:03 -04:00
Vitor Pamplona
e27efbc9c2 Privacy Policy:
- Removes restrictions of use from the F-Droid version given that F-Droid is taking that legal liability.
- Adds information about the Image Servers offered through the app.
- Adds privacy warnings from the readme to this document.
2023-06-12 10:23:29 -04:00
greenart7c3
57a5d0b618 Merge branch 'main' into main 2023-06-12 07:39:30 -03:00
greenart7c3
8f668cb5fb separated spam and report filter options 2023-06-12 07:38:32 -03:00
Vitor Pamplona
df5ea6f741 Adjusts the size and alignment of relay icons in the video feed 2023-06-11 10:59:18 -04:00
Vitor Pamplona
2e6847df1c v0.56.5 2023-06-10 21:20:57 -04:00
Vitor Pamplona
76a49d6556 Fixing upstream issue with images on markdown. 2023-06-10 21:18:30 -04:00
Vitor Pamplona
59fe3ccf3a Updates readme 2023-06-10 21:18:08 -04:00
Vitor Pamplona
ba884bd1aa Improving markdown support 2023-06-10 21:17:50 -04:00
Vitor Pamplona
e98bb76ee0 Support for NIP-14 2023-06-10 21:16:55 -04:00
Vitor Pamplona
e08bf3eb97 If compression fails, tries to upload with the original file 2023-06-10 21:15:19 -04:00
Vitor Pamplona
9ef02d63a0 Refactors color objects to avoid recreating them on-demand. 2023-06-10 10:08:06 -04:00
Vitor Pamplona
7007796497 v0.56.4 2023-06-09 18:16:42 -04:00
Vitor Pamplona
9f030ffbf8 BugFix for crashing on start up due to creating two caches for videos. 2023-06-09 18:16:03 -04:00
greenart7c3
f0638b1766 add option to opt-out from automatic spam and report filters 2023-06-09 08:28:05 -03:00
84 changed files with 1634 additions and 688 deletions

View File

@@ -1,13 +1,48 @@
# Amethyst Privacy Policy & Terms of Use
# Amethyst Privacy Policy and Terms of Use
Effective as of Jan 20, 2023 for the distributed applications in the Play Store and the F-Droid Catalogue
## Privacy Policy
The Amethyst app for Android does not collect or process any personal information from its users. The app is used to connect to third-party Nostr servers (also called Relays) that may or may not collect personal information and are not covered by this privacy policy. Each third-party relay server comes equipped with its own privacy policy and terms of use that can be viewed through the app or through that server's website. The developers of this open source project or maintainers of the distribution channels (app stores) do not have access to the data located in the user's phone. Accounts are fully maintained by the user. We do not have control over them.
Effective as of Jun 12, 2023
The app may collect a per-device token, your public key and a preferred Relay to connect to and provide push notification services through Google's Firebase Cloud Messaging. Other than that, the data from connected accounts is only stored locally on the device when it is required for functionality and performance of Amethyst. This data is strictly confidental and cannot be accessed by other apps (on non-rooted devices). Phone data can be deleted by clearing Amethyst's local storage or uninstalling the app.
The Amethyst app for Android does not collect or process any personal information from its users. The app is used to connect to third-party Nostr servers (also called Relays) that may or may not collect personal information and are not covered by this privacy policy. Each third-party relay server comes equipped with its own privacy policy and terms of use that can be viewed through the app or through that server's website. The developers of this open-source project or maintainers of the distribution channels (app stores) do not have access to the data located in the user's phone. Accounts are fully maintained by the user. We do not have control over them.
You cannot use the Amethyst app for Android to submit Objectionable Content to relays. Objectionable Content includes, but is not limited to: (i) sexually explicit materials; (ii) obscene, defamatory, libelous, slanderous, violent and/or unlawful content or profanity; (iii) content that infringes upon the rights of any third party, including copyright, trademark, privacy, publicity or other personal or proprietary right, or that is deceptive or fraudulent; (iv) content that promotes the use or sale of illegal or regulated substances, tobacco products, ammunition and/or firearms; and (v) illegal content related to gambling.
The app may collect a per-device token, your public key, and a preferred Relay to connect to and provide push notification services through Google's Firebase Cloud Messaging. Other than that, the data from connected accounts are only stored locally on the device when it's required for the functionality and performance of Amethyst. This data is strictly confidential and cannot be accessed by other apps (on non-rooted devices). Phone data can be deleted by clearing Amethyst's local storage or uninstalling the app.
We reserve the right to modify this Privacy Policy at any time. Any modifications to this Privacy Policy will be effective upon our posting the new terms and/or upon implementation of the new changes on the Service (or as otherwise indicated at the time of posting). In all cases, your continued use of the app after the posting of any modified Privacy Policy indicates your acceptance of the terms of the modified Privacy Policy.
Amethyst offers a few options to upload pictures and videos in order to post them online. You can choose the server at your discretion. Similar to relays, such services are independent of the app and have their own privacy policy and terms of use.
### Privacy with Relays services
Your internet protocol (IP) address is exposed to the relays you connect to. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online.
The relay can also see which public keys you are using and what information you are requesting from the network. Your public key is tied to your IP address and your relay filters.
Relays have all your data in raw text. They know your IP, your name, your location (guessed from IP), your pub key, all your contacts, and other relays, and can read every action you do (post, like, boost, quote, report, etc) with the exception of the content inside Private Zaps and Private DMs.
While the content of direct messages (DMs) is only visible to you, and your DM Nostr counterparty, everyone can see when you and your counterparty are DM-ing each other. Image uploads in the DM screen use one of the chosen image servers and simply paste the image link on the DM text. Your uploaded pictures are available to anyone with that direct link.
### Visibility & Permanence of Your Content on Nostr Relays
#### Information Visibility
Content that you share can be shared with other relays by any user of the network.
Information that you share is publicly visible to anyone reading from relays that have your information. Your information may also be visible to Nostr users who do not share relays with you.
#### 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.
## Terms of Use
### For versions downloaded from Google's Play Store
You cannot use the Amethyst app for Android to submit Objectionable Content to relays. Objectionable Content includes but is not limited to: (i) sexually explicit materials; (ii) obscene, defamatory, libelous, slanderous, violent and/or unlawful content or profanity; (iii) content that infringes upon the rights of any third party, including copyright, trademark, privacy, publicity or other personal or proprietary rights, or that is deceptive or fraudulent; (iv) content that promotes the use or sale of illegal or regulated substances, tobacco products, ammunition and/or firearms; and (v) illegal content related to gambling.
### For versions downloaded from F-Droid
We do not control the distribution of the application in F-Droid. Legal matters should be resolved between the user and F-Droid.
## Other Notes
We reserve the right to modify this Privacy Policy and Terms of Use at any time. Any modifications to this document will be effective upon our posting of the new terms and/or upon implementation of the new changes on the Service (or as otherwise indicated at the time of posting). In all cases, your continued use of the app after the posting of any modified Privacy Policy and Terms of Use indicates your acceptance of the terms of the modified Privacy Policy and/or Terms of Use.
If you have any questions about Amethyst or this privacy policy, you can send a message to amethyst@vitorpamplona.com

View File

@@ -62,13 +62,14 @@ Or get the latest APK from the [Releases Section](https://github.com/vitorpamplo
- [x] Generic Tags (NIP-12)
- [x] Sensitive Content (NIP-36)
- [x] View Individual Reactions (Like, Boost, Zaps, Reports) per Post
- [x] Recommended Application Handlers (NIP-89)
- [x] Events with a Subject (NIP-14)
- [ ] Marketplace (NIP-15)
- [ ] Image/Video Capture in the app
- [ ] Local Database
- [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51)
- [ ] Relay Pages (NIP-11)
- [ ] Proof of Work in the Phone (NIP-13, NIP-20)
- [ ] Events with a Subject (NIP-14)
- [ ] Workspaces
- [ ] Expiration Support (NIP-40)
- [ ] Infinity Scroll

View File

@@ -13,8 +13,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 33
versionCode 203
versionName "0.56.3"
versionCode 210
versionName "0.58.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -165,9 +165,14 @@ dependencies {
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
// Markdown
implementation "com.halilibo.compose-richtext:richtext-ui:0.16.0"
implementation "com.halilibo.compose-richtext:richtext-ui-material:0.16.0"
implementation "com.halilibo.compose-richtext:richtext-commonmark:0.16.0"
//implementation "com.halilibo.compose-richtext:richtext-ui:0.16.0"
//implementation "com.halilibo.compose-richtext:richtext-ui-material:0.16.0"
//implementation "com.halilibo.compose-richtext:richtext-commonmark:0.16.0"
// Markdown (With fix for full-image bleeds)
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui:main-SNAPSHOT')
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui-material:main-SNAPSHOT')
implementation('com.github.vitorpamplona.compose-richtext:richtext-commonmark:main-SNAPSHOT')
// Local model for language identification
playImplementation 'com.google.mlkit:language-id:17.0.4'

View File

@@ -49,6 +49,7 @@ private object PrefKeys {
const val LANGUAGE_PREFS = "languagePreferences"
const val TRANSLATE_TO = "translateTo"
const val ZAP_AMOUNTS = "zapAmounts"
const val REACTION_CHOICES = "reactionChoices"
const val DEFAULT_ZAPTYPE = "defaultZapType"
const val DEFAULT_FILE_SERVER = "defaultFileServer"
const val DEFAULT_HOME_FOLLOW_LIST = "defaultHomeFollowList"
@@ -61,6 +62,8 @@ private object PrefKeys {
const val USE_PROXY = "use_proxy"
const val PROXY_PORT = "proxy_port"
const val SHOW_SENSITIVE_CONTENT = "show_sensitive_content"
const val WARN_ABOUT_REPORTS = "warn_about_reports"
const val FILTER_SPAM_FROM_STRANGERS = "filter_spam_from_strangers"
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
}
@@ -206,6 +209,7 @@ object LocalPreferences {
putString(PrefKeys.LANGUAGE_PREFS, gson.toJson(account.languagePreferences))
putString(PrefKeys.TRANSLATE_TO, account.translateTo)
putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices))
putString(PrefKeys.REACTION_CHOICES, gson.toJson(account.reactionChoices))
putString(PrefKeys.DEFAULT_ZAPTYPE, gson.toJson(account.defaultZapType))
putString(PrefKeys.DEFAULT_FILE_SERVER, gson.toJson(account.defaultFileServer))
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, account.defaultHomeFollowList)
@@ -217,6 +221,8 @@ object LocalPreferences {
putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog)
putBoolean(PrefKeys.USE_PROXY, account.proxy != null)
putInt(PrefKeys.PROXY_PORT, account.proxyPort)
putBoolean(PrefKeys.WARN_ABOUT_REPORTS, account.warnAboutPostsWithReports)
putBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, account.filterSpamFromStrangers)
if (account.showSensitiveContent == null) {
remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
@@ -254,6 +260,11 @@ object LocalPreferences {
object : TypeToken<List<Long>>() {}.type
) ?: listOf(500L, 1000L, 5000L)
val reactionChoices = gson.fromJson<List<String>>(
getString(PrefKeys.REACTION_CHOICES, "[]"),
object : TypeToken<List<String>>() {}.type
).ifEmpty { listOf("+") } ?: listOf("+")
val defaultZapType = gson.fromJson(
getString(PrefKeys.DEFAULT_ZAPTYPE, "PUBLIC"),
object : TypeToken<LnZapEvent.ZapType>() {}.type
@@ -306,28 +317,33 @@ object LocalPreferences {
} else {
null
}
val filterSpam = getBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, true)
val warnAboutReports = getBoolean(PrefKeys.WARN_ABOUT_REPORTS, true)
val a = Account(
Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
followingChannels,
hiddenUsers,
localRelays,
dontTranslateFrom,
languagePreferences,
translateTo,
zapAmountChoices,
defaultZapType,
defaultFileServer,
defaultHomeFollowList,
defaultStoriesFollowList,
defaultNotificationFollowList,
zapPaymentRequestServer,
hideDeleteRequestDialog,
hideBlockAlertDialog,
latestContactList,
proxy,
proxyPort,
showSensitiveContent
loggedIn = Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
followingChannels = followingChannels,
hiddenUsers = hiddenUsers,
localRelays = localRelays,
dontTranslateFrom = dontTranslateFrom,
languagePreferences = languagePreferences,
translateTo = translateTo,
zapAmountChoices = zapAmountChoices,
reactionChoices = reactionChoices,
defaultZapType = defaultZapType,
defaultFileServer = defaultFileServer,
defaultHomeFollowList = defaultHomeFollowList,
defaultStoriesFollowList = defaultStoriesFollowList,
defaultNotificationFollowList = defaultNotificationFollowList,
zapPaymentRequest = zapPaymentRequestServer,
hideDeleteRequestDialog = hideDeleteRequestDialog,
hideBlockAlertDialog = hideBlockAlertDialog,
backupContactList = latestContactList,
proxy = proxy,
proxyPort = proxyPort,
showSensitiveContent = showSensitiveContent,
warnAboutPostsWithReports = warnAboutReports,
filterSpamFromStrangers = filterSpam
)
return a

View File

@@ -0,0 +1,11 @@
package com.vitorpamplona.amethyst
object OptOutFromFilters {
var warnAboutPostsWithReports: Boolean = true
var filterSpamFromStrangers: Boolean = true
fun start(warnAboutReports: Boolean, filterSpam: Boolean) {
warnAboutPostsWithReports = warnAboutReports
filterSpamFromStrangers = filterSpam
}
}

View File

@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst
import android.content.Context
import android.os.Build
import android.util.Log
import coil.Coil
import coil.ImageLoader
import coil.decode.GifDecoder
@@ -31,11 +32,15 @@ object ServiceManager {
start(context)
}
@Synchronized
fun start(context: Context) {
Log.d("ServiceManager", "Starting Relay Services")
val myAccount = account
// Resets Proxy Use
HttpClient.start(account)
OptOutFromFilters.start(account?.warnAboutPostsWithReports ?: true, account?.filterSpamFromStrangers ?: true)
Coil.setImageLoader {
ImageLoader.Builder(context).components {
if (Build.VERSION.SDK_INT >= 28) {
@@ -75,6 +80,8 @@ object ServiceManager {
}
fun pause() {
Log.d("ServiceManager", "Pausing Relay Services")
NostrAccountDataSource.stop()
NostrHomeDataSource.stop()
NostrChannelDataSource.stop()

View File

@@ -19,6 +19,7 @@ object VideoCache {
lateinit var cacheDataSourceFactory: CacheDataSource.Factory
@Synchronized
fun init(context: Context) {
if (!this::simpleCache.isInitialized) {
exoDatabaseProvider = StandaloneDatabaseProvider(context)

View File

@@ -6,6 +6,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.core.os.ConfigurationCompat
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.OptOutFromFilters
import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
import com.vitorpamplona.amethyst.service.model.*
@@ -55,6 +56,7 @@ class Account(
var languagePreferences: Map<String, String> = mapOf(),
var translateTo: String = Locale.getDefault().language,
var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L),
var reactionChoices: List<String> = listOf("+"),
var defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PRIVATE,
var defaultFileServer: ServersAvailable = ServersAvailable.NOSTR_BUILD,
var defaultHomeFollowList: String = KIND3_FOLLOWS,
@@ -66,7 +68,9 @@ class Account(
var backupContactList: ContactListEvent? = null,
var proxy: Proxy?,
var proxyPort: Int,
var showSensitiveContent: Boolean? = null
var showSensitiveContent: Boolean? = null,
var warnAboutPostsWithReports: Boolean = true,
var filterSpamFromStrangers: Boolean = true
) {
var transientHiddenUsers: Set<String> = setOf()
@@ -77,6 +81,17 @@ class Account(
var userProfileCache: User? = null
fun updateOptOutOptions(warnReports: Boolean, filterSpam: Boolean) {
warnAboutPostsWithReports = warnReports
filterSpamFromStrangers = filterSpam
OptOutFromFilters.start(warnAboutPostsWithReports, filterSpamFromStrangers)
if (!filterSpamFromStrangers) {
transientHiddenUsers = setOf()
}
live.invalidateData()
saveable.invalidateData()
}
fun userProfile(): User {
return userProfileCache ?: run {
val myUser: User = LocalCache.getOrCreateUser(loggedIn.pubKey.toHexKey())
@@ -129,8 +144,8 @@ class Account(
}
}
fun reactionTo(note: Note): List<Note> {
return note.reactedBy(userProfile(), "+")
fun reactionTo(note: Note, reaction: String): List<Note> {
return note.reactedBy(userProfile(), reaction)
}
fun hasBoosted(note: Note): Boolean {
@@ -141,20 +156,20 @@ class Account(
return note.boostedBy(userProfile())
}
fun hasReacted(note: Note): Boolean {
return note.hasReacted(userProfile(), "+")
fun hasReacted(note: Note, reaction: String): Boolean {
return note.hasReacted(userProfile(), reaction)
}
fun reactTo(note: Note) {
fun reactTo(note: Note, reaction: String) {
if (!isWriteable()) return
if (hasReacted(note)) {
if (hasReacted(note, reaction)) {
// has already liked this note
return
}
note.event?.let {
val event = ReactionEvent.createLike(it, loggedIn.privKey!!)
val event = ReactionEvent.create(reaction, it, loggedIn.privKey!!)
Client.send(event)
LocalCache.consume(event)
}
@@ -483,7 +498,10 @@ class Account(
mentions: List<User>?,
tags: List<String>? = null,
zapReceiver: String? = null,
wantsToMarkAsSensitive: Boolean
wantsToMarkAsSensitive: Boolean,
replyingTo: String?,
root: String?,
directMentions: Set<HexKey>
) {
if (!isWriteable()) return
@@ -499,6 +517,9 @@ class Account(
extraTags = tags,
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
replyingTo = replyingTo,
root = root,
directMentions = directMentions,
privateKey = loggedIn.privKey!!
)
@@ -843,6 +864,12 @@ class Account(
saveable.invalidateData()
}
fun changeReactionTypes(newTypes: List<String>) {
reactionChoices = newTypes
live.invalidateData()
saveable.invalidateData()
}
fun changeZapPaymentRequest(newServer: Nip47URI?) {
zapPaymentRequest = newServer
live.invalidateData()
@@ -1057,12 +1084,19 @@ class Account(
}
fun isAcceptable(user: User): Boolean {
if (!warnAboutPostsWithReports) {
return !isHidden(user) && // if user hasn't hided this author
user.reportsBy(userProfile()).isEmpty() // if user has not reported this post
}
return !isHidden(user) && // if user hasn't hided this author
user.reportsBy(userProfile()).isEmpty() && // if user has not reported this post
user.countReportAuthorsBy(followingKeySet()) < 5
}
fun isAcceptableDirect(note: Note): Boolean {
if (!warnAboutPostsWithReports) {
return note.reportsBy(userProfile()).isEmpty()
}
return note.reportsBy(userProfile()).isEmpty() && // if user has not reported this post
note.countReportAuthorsBy(followingKeySet()) < 5 // if it has 5 reports by reliable users
}

View File

@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.model
import android.util.Log
import android.util.LruCache
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.OptOutFromFilters
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.nip19.Nip19
@@ -20,6 +21,8 @@ class AntiSpamFilter {
fun isSpam(event: Event, relay: Relay?): Boolean {
checkNotInMainThread()
if (!OptOutFromFilters.filterSpamFromStrangers) return false
val idHex = event.id
// if short message, ok

View File

@@ -51,6 +51,18 @@ object LocalCache {
}
}
fun getUserIfExists(key: String): User? {
return users[key]
}
fun getNoteIfExists(key: String): Note? {
return addressables[key] ?: notes[key]
}
fun getChannelIfExists(key: String): Channel? {
return channels[key]
}
fun checkGetOrCreateNote(key: String): Note? {
checkNotInMainThread()
@@ -542,17 +554,8 @@ object LocalCache {
// Log.d("RE", "New Reaction ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
if (
event.content == "" ||
event.content == "+" ||
event.content == "\u2764\uFE0F" || // red heart
event.content == "\uD83E\uDD19" || // call me hand
event.content == "\uD83D\uDC4D" // thumbs up
) {
// Counts the replies
repliesTo.forEach {
it.addReaction(note)
}
repliesTo.forEach {
it.addReaction(note)
}
refreshObservers(note)

View File

@@ -43,7 +43,7 @@ open class Note(val idHex: String) {
// These fields are updated every time an event related to this note is received.
var replies = setOf<Note>()
private set
var reactions = setOf<Note>()
var reactions = mapOf<String, Set<Note>>()
private set
var boosts = setOf<Note>()
private set
@@ -134,8 +134,20 @@ open class Note(val idHex: String) {
liveSet?.boosts?.invalidateData()
}
fun removeReaction(note: Note) {
reactions = reactions - note
liveSet?.reactions?.invalidateData()
val reaction = note.event?.content() ?: "+"
if (reaction in reactions.keys && reactions[reaction]?.contains(note) == true) {
reactions[reaction]?.let {
val newList = it.minus(note)
if (newList.isEmpty()) {
reactions = reactions.minus(reaction)
} else {
reactions = reactions + Pair(reaction, newList)
}
liveSet?.reactions?.invalidateData()
}
}
}
fun removeReport(deleteNote: Note) {
@@ -203,8 +215,13 @@ open class Note(val idHex: String) {
}
fun addReaction(note: Note) {
if (note !in reactions) {
reactions = reactions + note
val reaction = note.event?.content() ?: "+"
if (reaction !in reactions.keys) {
reactions = reactions + Pair(reaction, setOf(note))
liveSet?.reactions?.invalidateData()
} else if (reactions[reaction]?.contains(note) == false) {
reactions = reactions + Pair(reaction, (reactions[reaction] ?: emptySet()) + note)
liveSet?.reactions?.invalidateData()
}
}
@@ -243,8 +260,10 @@ open class Note(val idHex: String) {
}
}
fun isReactedBy(user: User): Boolean {
return reactions.any { it.author?.pubkeyHex == user.pubkeyHex }
fun isReactedBy(user: User): String? {
return reactions.filter {
it.value.any { it.author?.pubkeyHex == user.pubkeyHex }
}.keys.firstOrNull()
}
fun isBoostedBy(user: User): Boolean {
@@ -269,6 +288,10 @@ open class Note(val idHex: String) {
}.flatten()
}
fun countReactions(): Int {
return reactions.values.sumOf { it.size }
}
fun zappedAmount(privKey: ByteArray?, walletServicePubkey: ByteArray?): BigDecimal {
// Regular Zap Receipts
val completedZaps = zaps.asSequence()
@@ -361,7 +384,11 @@ open class Note(val idHex: String) {
}
fun reactedBy(loggedIn: User, content: String): List<Note> {
return reactions.filter { it.author == loggedIn && it.event?.content() == content }
return reactions[content]?.filter { it.author == loggedIn && it.event?.content() == content } ?: emptyList()
}
fun reactedBy(loggedIn: User): List<String> {
return reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key }
}
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {

View File

@@ -1,26 +1,21 @@
package com.vitorpamplona.amethyst.model
import android.util.LruCache
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.previews.BahaUrlPreview
import com.vitorpamplona.amethyst.service.previews.IUrlPreviewCallback
import com.vitorpamplona.amethyst.service.previews.UrlInfoItem
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
object UrlCachedPreviewer {
var cache = mapOf<String, UrlInfoItem>()
private set
var failures = mapOf<String, Throwable>()
var cache = LruCache<String, UrlPreviewState>(100)
private set
fun previewInfo(url: String, callback: IUrlPreviewCallback? = null) {
fun previewInfo(url: String, onReady: (UrlPreviewState) -> Unit) {
checkNotInMainThread()
cache[url]?.let {
callback?.onComplete(it)
return
}
failures[url]?.let {
callback?.onFailed(it)
onReady(it)
return
}
@@ -28,13 +23,32 @@ object UrlCachedPreviewer {
url,
object : IUrlPreviewCallback {
override fun onComplete(urlInfo: UrlInfoItem) {
cache = cache + Pair(url, urlInfo)
callback?.onComplete(urlInfo)
cache[url]?.let {
if (it is UrlPreviewState.Loaded || it is UrlPreviewState.Empty) {
onReady(it)
return
}
}
val state = if (urlInfo.allFetchComplete() && urlInfo.url == url) {
UrlPreviewState.Loaded(urlInfo)
} else {
UrlPreviewState.Empty
}
cache.put(url, state)
onReady(state)
}
override fun onFailed(throwable: Throwable) {
failures = failures + Pair(url, throwable)
callback?.onFailed(throwable)
cache[url]?.let {
onReady(it)
return
}
val state = UrlPreviewState.Error(throwable.message ?: "Error Loading url preview")
cache.put(url, state)
onReady(state)
}
}
).fetchUrlPreview()

View File

@@ -19,6 +19,10 @@ class TextNoteEvent(
sig: HexKey
) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun subject() = tags.firstOrNull() { it.size > 1 && it[0] == "subject" }?.get(1)
fun root() = tags.firstOrNull() { it.size > 3 && it[3] == "root" }?.get(1)
companion object {
const val kind = 1
@@ -30,19 +34,44 @@ class TextNoteEvent(
extraTags: List<String>?,
zapReceiver: String?,
markAsSensitive: Boolean,
replyingTo: String?,
root: String?,
directMentions: Set<HexKey>,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
): TextNoteEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()
replyTos?.forEach {
tags.add(listOf("e", it))
if (it == replyingTo) {
tags.add(listOf("e", it, "", "reply"))
} else if (it == root) {
tags.add(listOf("e", it, "", "root"))
} else if (it in directMentions) {
tags.add(listOf("e", it, "", "mention"))
} else {
tags.add(listOf("e", it))
}
}
mentions?.forEach {
tags.add(listOf("p", it))
if (it in directMentions) {
tags.add(listOf("p", it, "", "mention"))
} else {
tags.add(listOf("p", it))
}
}
addresses?.forEach {
tags.add(listOf("a", it.toTag()))
val aTag = it.toTag()
if (aTag == replyingTo) {
tags.add(listOf("a", aTag, "", "reply"))
} else if (aTag == root) {
tags.add(listOf("a", aTag, "", "root"))
} else if (aTag in directMentions) {
tags.add(listOf("a", aTag, "", "mention"))
} else {
tags.add(listOf("a", aTag))
}
}
findHashtags(msg).forEach {
tags.add(listOf("t", it))

View File

@@ -7,6 +7,7 @@ import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Bundle
import android.util.Log
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.fillMaxSize
@@ -108,9 +109,10 @@ class MainActivity : FragmentActivity() {
// network is available for use
override fun onAvailable(network: Network) {
super.onAvailable(network)
Log.d("NETWORKCALLBACK", "onAvailable: Disconnecting and connecting again")
// Only starts after login
GlobalScope.launch(Dispatchers.IO) {
ServiceManager.pause()
ServiceManager.start(this@MainActivity)
}
}
@@ -126,7 +128,7 @@ class MainActivity : FragmentActivity() {
// lost network connection
override fun onLost(network: Network) {
super.onLost(network)
Log.d("NETWORKCALLBACK", "onLost: Disconnecting and pausing relay's connection")
// Only starts after login
GlobalScope.launch(Dispatchers.IO) {
ServiceManager.pause()

View File

@@ -64,6 +64,7 @@ import com.vitorpamplona.amethyst.ui.note.UserPicture
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.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
@@ -128,7 +129,7 @@ fun JoinUserOrChannelView(searchBarViewModel: SearchBarViewModel, onClose: () ->
Text(
text = "",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
fontWeight = FontWeight.Bold
)
}
@@ -270,7 +271,7 @@ private fun SearchEditTextForJoin(
placeholder = {
Text(
text = stringResource(R.string.channel_list_user_or_group_id_demo),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
trailingIcon = {
@@ -416,7 +417,7 @@ private fun DisplayUserAboutInfo(baseUser: User) {
Text(
text = about,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)

View File

@@ -27,6 +27,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -79,7 +80,7 @@ fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, chan
placeholder = {
Text(
text = stringResource(R.string.my_awesome_group),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -98,7 +99,7 @@ fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, chan
placeholder = {
Text(
text = "http://mygroup.com/logo.jpg",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
}
)
@@ -115,7 +116,7 @@ fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, chan
placeholder = {
Text(
text = stringResource(R.string.about_us),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(

View File

@@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.components.*
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -221,7 +222,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, acc: Account) {
placeholder = {
Text(
text = stringResource(R.string.content_description_example),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(

View File

@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.actions
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
@@ -8,11 +9,16 @@ import com.vitorpamplona.amethyst.service.nip19.Nip19
class NewMessageTagger(var channelHex: String?, var mentions: List<User>?, var replyTos: List<Note>?, var message: String) {
val directMentions = mutableSetOf<HexKey>()
fun addUserToMentions(user: User) {
directMentions.add(user.pubkeyHex)
mentions = if (mentions?.contains(user) == true) mentions else mentions?.plus(user) ?: listOf(user)
}
fun addNoteToReplyTos(note: Note) {
directMentions.add(note.idHex)
note.author?.let { addUserToMentions(it) }
replyTos = if (replyTos?.contains(note) == true) replyTos else replyTos?.plus(note) ?: listOf(note)
}

View File

@@ -22,6 +22,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
fun NewPollClosing(pollViewModel: NewPostViewModel) {
@@ -43,7 +44,7 @@ fun NewPollClosing(pollViewModel: NewPostViewModel) {
)
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colors.primary,
unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
unfocusedBorderColor = MaterialTheme.colors.placeholderText
)
Row(
@@ -59,13 +60,13 @@ fun NewPollClosing(pollViewModel: NewPostViewModel) {
label = {
Text(
text = stringResource(R.string.poll_closing_time),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
placeholder = {
Text(
text = stringResource(R.string.poll_closing_time_days),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
}
)

View File

@@ -22,6 +22,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
fun NewPollConsensusThreshold(pollViewModel: NewPostViewModel) {
@@ -43,7 +44,7 @@ fun NewPollConsensusThreshold(pollViewModel: NewPostViewModel) {
)
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colors.primary,
unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
unfocusedBorderColor = MaterialTheme.colors.placeholderText
)
Row(
@@ -59,13 +60,13 @@ fun NewPollConsensusThreshold(pollViewModel: NewPostViewModel) {
label = {
Text(
text = stringResource(R.string.poll_consensus_threshold),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
placeholder = {
Text(
text = stringResource(R.string.poll_consensus_threshold_percent),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
}
)

View File

@@ -11,6 +11,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.Preview
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
fun NewPollOption(pollViewModel: NewPostViewModel, optionIndex: Int) {
@@ -35,13 +36,13 @@ fun NewPollOption(pollViewModel: NewPostViewModel, optionIndex: Int) {
label = {
Text(
text = stringResource(R.string.poll_option_index).format(optionIndex + 1),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
placeholder = {
Text(
text = stringResource(R.string.poll_option_description),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(

View File

@@ -19,6 +19,7 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@OptIn(ExperimentalComposeUiApi::class)
@Composable
@@ -38,7 +39,7 @@ fun NewPollPrimaryDescription(pollViewModel: NewPostViewModel) {
)
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colors.primary,
unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
unfocusedBorderColor = MaterialTheme.colors.placeholderText
)
OutlinedTextField(
@@ -49,7 +50,7 @@ fun NewPollPrimaryDescription(pollViewModel: NewPostViewModel) {
label = {
Text(
text = stringResource(R.string.poll_primary_description),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -67,7 +68,7 @@ fun NewPollPrimaryDescription(pollViewModel: NewPostViewModel) {
placeholder = {
Text(
text = stringResource(R.string.poll_primary_description),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
colors = if (isInputValid) colorValid else colorInValid,

View File

@@ -10,6 +10,7 @@ import androidx.compose.ui.res.stringResource
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
fun NewPollRecipientsField(pollViewModel: NewPostViewModel, account: Account) {
@@ -29,13 +30,13 @@ fun NewPollRecipientsField(pollViewModel: NewPostViewModel, account: Account) {
label = {
Text(
text = stringResource(R.string.poll_zap_recipients),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
placeholder = {
Text(
text = stringResource(R.string.poll_zap_recipients),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
}

View File

@@ -21,6 +21,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
@@ -71,7 +72,7 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
)
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colors.primary,
unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
unfocusedBorderColor = MaterialTheme.colors.placeholderText
)
Row(
@@ -87,13 +88,13 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
label = {
Text(
text = stringResource(R.string.poll_zap_value_min),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
placeholder = {
Text(
text = stringResource(R.string.sats),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
}
)
@@ -106,13 +107,13 @@ fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
label = {
Text(
text = stringResource(R.string.poll_zap_value_max),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
placeholder = {
Text(
text = stringResource(R.string.sats),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
}
)

View File

@@ -73,6 +73,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
@@ -200,7 +203,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
placeholder = {
Text(
text = stringResource(R.string.what_s_on_your_mind),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
colors = TextFieldDefaults
@@ -219,9 +222,9 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
Button(
onClick = { postViewModel.pollOptions[postViewModel.pollOptions.size] = "" },
border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f)),
border = BorderStroke(1.dp, MaterialTheme.colors.placeholderText),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
contentColor = MaterialTheme.colors.placeholderText
)
) {
Image(
@@ -293,7 +296,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
MaterialTheme.colors.subtleBorder,
QuoteBorder
)
)
@@ -395,7 +398,7 @@ fun Notifying(baseMentions: ImmutableList<User>?, onClick: (User) -> Unit) {
Text(
stringResource(R.string.reply_notify),
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
mentions.forEachIndexed { idx, user ->
@@ -410,7 +413,7 @@ fun Notifying(baseMentions: ImmutableList<User>?, onClick: (User) -> Unit) {
Button(
shape = ButtonBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary.copy(alpha = 0.32f)
backgroundColor = MaterialTheme.colors.mediumImportanceLink
),
onClick = {
onClick(myUser)
@@ -551,7 +554,7 @@ private fun ForwardZapTo(
placeholder = {
Text(
text = stringResource(R.string.zap_forward_lnAddress),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
fontSize = 14.sp
)
},
@@ -774,7 +777,7 @@ fun ImageVideoDescription(
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
MaterialTheme.colors.subtleBorder,
QuoteBorder
)
) {
@@ -819,7 +822,7 @@ fun ImageVideoDescription(
modifier = Modifier
.padding(end = 5.dp)
.size(30.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
tint = MaterialTheme.colors.placeholderText
)
}
}
@@ -909,7 +912,7 @@ fun ImageVideoDescription(
placeholder = {
Text(
text = stringResource(R.string.content_description_example),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(

View File

@@ -137,7 +137,24 @@ open class NewPostViewModel() : ViewModel() {
} else if (originalNote?.event is PrivateDmEvent) {
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive)
} else {
account?.sendPost(tagger.message, tagger.replyTos, tagger.mentions, null, zapReceiver, wantsToMarkAsSensitive)
// adds markers
val rootId =
(originalNote?.event as? TextNoteEvent)?.root() // if it has a marker as root
?: originalNote?.replyTo?.firstOrNull { it.event != null && it.replyTo?.isEmpty() == true }?.idHex // if it has loaded events with zero replies in the reply list
?: originalNote?.replyTo?.firstOrNull()?.idHex // old rules, first item is root.
val replyId = originalNote?.idHex
account?.sendPost(
message = tagger.message,
replyTo = tagger.replyTos,
mentions = tagger.mentions,
tags = null,
zapReceiver = zapReceiver,
wantsToMarkAsSensitive = wantsToMarkAsSensitive,
replyingTo = replyId,
root = rootId,
directMentions = tagger.directMentions
)
}
cancel()

View File

@@ -52,6 +52,7 @@ import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import java.lang.Math.round
@Composable
@@ -157,7 +158,7 @@ fun ServerConfigHeader() {
maxLines = 1,
fontSize = 14.sp,
modifier = Modifier.weight(1.2f),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
Spacer(modifier = Modifier.size(5.dp))
@@ -167,7 +168,7 @@ fun ServerConfigHeader() {
maxLines = 1,
fontSize = 14.sp,
modifier = Modifier.weight(1.2f),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
Spacer(modifier = Modifier.size(5.dp))
@@ -177,7 +178,7 @@ fun ServerConfigHeader() {
maxLines = 1,
fontSize = 14.sp,
modifier = Modifier.weight(1f),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
Spacer(modifier = Modifier.size(5.dp))
@@ -187,7 +188,7 @@ fun ServerConfigHeader() {
maxLines = 1,
fontSize = 14.sp,
modifier = Modifier.weight(1f),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
Spacer(modifier = Modifier.size(2.dp))
@@ -375,7 +376,7 @@ fun ServerConfig(
maxLines = 1,
fontSize = 12.sp,
modifier = Modifier.weight(1.2f),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
IconButton(
@@ -403,7 +404,7 @@ fun ServerConfig(
maxLines = 1,
fontSize = 12.sp,
modifier = Modifier.weight(1.2f),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
Icon(
@@ -420,7 +421,7 @@ fun ServerConfig(
maxLines = 1,
fontSize = 12.sp,
modifier = Modifier.weight(1f),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
Icon(
@@ -435,7 +436,7 @@ fun ServerConfig(
maxLines = 1,
fontSize = 12.sp,
modifier = Modifier.weight(1f),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
}
}
@@ -464,7 +465,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni
placeholder = {
Text(
text = "server.com",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
maxLines = 1
)
},
@@ -478,7 +479,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni
modifier = Modifier
.size(35.dp)
.padding(horizontal = 5.dp),
tint = if (read) Color.Green else MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
tint = if (read) Color.Green else MaterialTheme.colors.placeholderText
)
}
@@ -489,7 +490,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni
modifier = Modifier
.size(35.dp)
.padding(horizontal = 5.dp),
tint = if (write) Color.Green else MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
tint = if (write) Color.Green else MaterialTheme.colors.placeholderText
)
}
@@ -507,7 +508,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = if (url.isNotBlank()) MaterialTheme.colors.primary else MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
backgroundColor = if (url.isNotBlank()) MaterialTheme.colors.primary else MaterialTheme.colors.placeholderText
)
) {
Text(text = stringResource(id = R.string.add), color = Color.White)

View File

@@ -28,6 +28,7 @@ import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -95,7 +96,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = stringResource(R.string.my_display_name),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -114,7 +115,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = stringResource(R.string.my_username),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
singleLine = true
@@ -133,7 +134,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = stringResource(id = R.string.about_me),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -152,13 +153,13 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = "https://mywebsite.com/me.jpg",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
leadingIcon = {
UploadFromGallery(
isUploading = postViewModel.isUploadingImageForPicture,
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
tint = MaterialTheme.colors.placeholderText,
modifier = Modifier.padding(start = 5.dp)
) {
postViewModel.uploadForPicture(it, context)
@@ -177,13 +178,13 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = "https://mywebsite.com/mybanner.jpg",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
leadingIcon = {
UploadFromGallery(
isUploading = postViewModel.isUploadingImageForBanner,
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
tint = MaterialTheme.colors.placeholderText,
modifier = Modifier.padding(start = 5.dp)
) {
postViewModel.uploadForBanner(it, context)
@@ -202,7 +203,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = "https://mywebsite.com",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
singleLine = true
@@ -218,7 +219,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = "_@mywebsite.com",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
singleLine = true
@@ -233,7 +234,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = "me@mylightiningnode.com",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
singleLine = true
@@ -249,7 +250,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = stringResource(R.string.lnurl),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
}
)
@@ -264,7 +265,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = stringResource(R.string.twitter_proof_url_template),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
}
)
@@ -279,7 +280,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = stringResource(R.string.mastodon_proof_url_template),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
}
)
@@ -294,7 +295,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
placeholder = {
Text(
text = stringResource(R.string.github_proof_url_template),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
}
)

View File

@@ -90,7 +90,7 @@ private fun LoadNote(
hex: String,
content: @Composable (Note) -> Unit
) {
var noteBase by remember(hex) { mutableStateOf<Note?>(null) }
var noteBase by remember(hex) { mutableStateOf(LocalCache.getNoteIfExists(hex)) }
LaunchedEffect(key1 = hex) {
if (noteBase == null) {
@@ -184,11 +184,13 @@ private fun DisplayAddress(
nip19: Nip19.Return,
nav: (String) -> Unit
) {
var noteBase by remember(nip19) { mutableStateOf<Note?>(null) }
var noteBase by remember(nip19) { mutableStateOf(LocalCache.getNoteIfExists(nip19.hex)) }
LaunchedEffect(key1 = nip19.hex) {
launch(Dispatchers.IO) {
noteBase = LocalCache.checkGetOrCreateAddressableNote(nip19.hex)
if (noteBase == null) {
launch(Dispatchers.IO) {
noteBase = LocalCache.checkGetOrCreateAddressableNote(nip19.hex)
}
}
}
@@ -221,32 +223,18 @@ private fun DisplayUser(
nip19: Nip19.Return,
nav: (String) -> Unit
) {
var userBase by remember(nip19) { mutableStateOf<User?>(null) }
var userBase by remember(nip19) { mutableStateOf(LocalCache.getUserIfExists(nip19.hex)) }
LaunchedEffect(key1 = nip19.hex) {
launch(Dispatchers.IO) {
userBase = LocalCache.checkGetOrCreateUser(nip19.hex)
if (userBase == null) {
launch(Dispatchers.IO) {
userBase = LocalCache.checkGetOrCreateUser(nip19.hex)
}
}
}
userBase?.let {
val userState by it.live().metadata.observeAsState()
val route = remember(userState) { "User/${it.pubkeyHex}" }
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() }
val addedCharts = remember {
"${nip19.additionalChars} "
}
if (userDisplayName != null) {
CreateClickableTextWithEmoji(
clickablePart = userDisplayName,
suffix = addedCharts,
tags = userTags,
route = route,
nav = nav
)
}
RenderUserAsClickableText(it, nip19, nav)
}
if (userBase == null) {
@@ -258,6 +246,42 @@ private fun DisplayUser(
}
}
@Composable
private fun RenderUserAsClickableText(
baseUser: User,
nip19: Nip19.Return,
nav: (String) -> Unit
) {
val userState by baseUser.live().metadata.observeAsState()
val route = remember { "User/${baseUser.pubkeyHex}" }
val userDisplayName by remember(userState) {
derivedStateOf {
userState?.user?.toBestDisplayName()
}
}
val userTags by remember(userState) {
derivedStateOf {
userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists()
}
}
val addedCharts = remember(nip19) {
"${nip19.additionalChars} "
}
userDisplayName?.let {
CreateClickableTextWithEmoji(
clickablePart = it,
suffix = addedCharts,
tags = userTags,
route = route,
nav = nav
)
}
}
@Composable
fun CreateClickableText(
clickablePart: String,

View File

@@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
@@ -23,13 +22,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground
const val SHORT_TEXT_LENGTH = 350
@@ -101,14 +100,13 @@ fun ExpandableRichTextViewer(
}
@Composable
public fun ShowMoreButton(onClick: () -> Unit) {
fun ShowMoreButton(onClick: () -> Unit) {
Button(
modifier = Modifier.padding(top = 10.dp),
onClick = onClick,
shape = ButtonBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary.copy(alpha = 0.32f)
.compositeOver(MaterialTheme.colors.background)
backgroundColor = MaterialTheme.colors.secondaryButtonBackground
),
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
) {

View File

@@ -25,6 +25,7 @@ import androidx.core.content.ContextCompat.startActivity
import com.vitorpamplona.amethyst.R
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 java.text.NumberFormat
@@ -67,7 +68,7 @@ fun InvoicePreview(lnInvoice: String, amount: String?) {
.fillMaxWidth()
.padding(start = 30.dp, end = 30.dp)
.clip(shape = QuoteBorder)
.border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), QuoteBorder)
.border(1.dp, MaterialTheme.colors.subtleBorder, QuoteBorder)
) {
Column(
modifier = Modifier

View File

@@ -37,6 +37,8 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import kotlinx.coroutines.launch
@Composable
@@ -57,7 +59,7 @@ fun InvoiceRequest(
.fillMaxWidth()
.padding(start = 30.dp, end = 30.dp)
.clip(shape = QuoteBorder)
.border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), QuoteBorder)
.border(1.dp, MaterialTheme.colors.subtleBorder, QuoteBorder)
) {
Column(
modifier = Modifier
@@ -98,7 +100,7 @@ fun InvoiceRequest(
placeholder = {
Text(
text = stringResource(R.string.thank_you_so_much),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(
@@ -123,7 +125,7 @@ fun InvoiceRequest(
placeholder = {
Text(
text = "1000",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
keyboardOptions = KeyboardOptions.Default.copy(

View File

@@ -58,7 +58,8 @@ class MediaCompressor {
}
override fun onFailure(index: Int, failureMessage: String) {
onError(failureMessage)
// keeps going with original video
onReady(uri, contentType, null)
}
override fun onCancelled(index: Int) {

View File

@@ -12,6 +12,7 @@ import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.Icon
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
@@ -23,11 +24,15 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontFamily
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.em
import androidx.compose.ui.unit.sp
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
@@ -43,6 +48,7 @@ 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.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.amethyst.ui.uriToRoute
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
@@ -83,6 +89,36 @@ 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 {
@@ -404,6 +440,8 @@ fun RenderCustomEmoji(word: String, state: RichTextViewerState) {
@Composable
private fun RenderContentAsMarkdown(content: String, backgroundColor: MutableState<Color>, tags: ImmutableListOfLists<String>?, nav: (String) -> Unit) {
val myMarkDownStyle = richTextDefaults.copy(
paragraphSpacing = DefaultParagraphSpacing,
headingStyle = DefaultHeadingStyle,
codeBlockStyle = richTextDefaults.codeBlockStyle?.copy(
textStyle = TextStyle(
fontFamily = FontFamily.Monospace,
@@ -415,7 +453,7 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: MutableSta
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
MaterialTheme.colors.subtleBorder,
QuoteBorder
)
.background(
@@ -448,15 +486,17 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: MutableSta
Unit
}
MaterialRichText(
style = myMarkDownStyle
) {
RefreshableContent(content, tags) {
Markdown(
content = it,
markdownParseOptions = MarkdownParseOptions.Default,
onLinkClicked = onClick
)
ProvideTextStyle(TextStyle(lineHeight = 1.30.em)) {
MaterialRichText(
style = myMarkDownStyle
) {
RefreshableContent(content, tags) {
Markdown(
content = it,
markdownParseOptions = MarkdownParseOptions.Default,
onLinkClicked = onClick
)
}
}
}
}
@@ -508,7 +548,7 @@ private fun ObserveNIP19Event(
it: Nip19.Return,
onRefresh: () -> Unit
) {
var baseNote by remember(it) { mutableStateOf<Note?>(null) }
var baseNote by remember(it) { mutableStateOf<Note?>(LocalCache.getNoteIfExists(it.hex)) }
LaunchedEffect(key1 = it.hex) {
if (baseNote == null) {
@@ -523,13 +563,18 @@ private fun ObserveNIP19Event(
}
baseNote?.let { note ->
val noteState by note.live().metadata.observeAsState()
ObserveNote(note, onRefresh)
}
}
LaunchedEffect(key1 = noteState) {
if (noteState?.note?.event != null) {
launch(Dispatchers.IO) {
onRefresh()
}
@Composable
fun ObserveNote(note: Note, onRefresh: () -> Unit) {
val noteState by note.live().metadata.observeAsState()
LaunchedEffect(key1 = noteState) {
if (noteState?.note?.event != null) {
launch(Dispatchers.IO) {
onRefresh()
}
}
}
@@ -540,7 +585,7 @@ private fun ObserveNIP19User(
it: Nip19.Return,
onRefresh: () -> Unit
) {
var baseUser by remember(it) { mutableStateOf<User?>(null) }
var baseUser by remember(it) { mutableStateOf<User?>(LocalCache.getUserIfExists(it.hex)) }
LaunchedEffect(key1 = it.hex) {
if (baseUser == null) {
@@ -555,13 +600,18 @@ private fun ObserveNIP19User(
}
baseUser?.let { user ->
val userState by user.live().metadata.observeAsState()
ObserveUser(user, onRefresh)
}
}
LaunchedEffect(key1 = userState) {
if (userState?.user?.info != null) {
launch(Dispatchers.IO) {
onRefresh()
}
@Composable
private fun ObserveUser(user: User, onRefresh: () -> Unit) {
val userState by user.live().metadata.observeAsState()
LaunchedEffect(key1 = userState) {
if (userState?.user?.info != null) {
launch(Dispatchers.IO) {
onRefresh()
}
}
}
@@ -651,7 +701,7 @@ private fun returnMarkdownWithSpecialContent(content: String, tags: ImmutableLis
if (isValidURL(word)) {
val removedParamsFromUrl = word.split("?")[0].lowercase()
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
returnContent += "$word "
returnContent += "![]($word) "
} else {
returnContent += "[$word]($word) "
}
@@ -767,7 +817,7 @@ private fun DisplayFullNote(
nav: (String) -> Unit,
loadedLink: LoadedBechLink
) {
val borderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
val borderColor = MaterialTheme.colors.subtleBorder
val modifier = remember {
Modifier
@@ -955,7 +1005,7 @@ private fun DisplayNoteFromTag(
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
MaterialTheme.colors.subtleBorder,
QuoteBorder
),
parentBackgroundColor = backgroundColor,

View File

@@ -25,6 +25,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -68,7 +69,7 @@ fun DotsIndicator(
totalDots: Int,
selectedIndex: Int,
selectedColor: Color = MaterialTheme.colors.primary /* Color.Yellow */,
unSelectedColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) /* Color.Gray */,
unSelectedColor: Color = MaterialTheme.colors.placeholderText /* Color.Gray */,
dotSize: Dp
) {
LazyRow(

View File

@@ -8,56 +8,25 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.service.previews.IUrlPreviewCallback
import com.vitorpamplona.amethyst.service.previews.UrlInfoItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun UrlPreview(url: String, urlText: String) {
val context = LocalContext.current
var urlPreviewState by remember(url) {
val default = UrlCachedPreviewer.cache[url]?.let {
if (it.allFetchComplete() && it.url == url) {
UrlPreviewState.Loaded(it)
} else {
UrlPreviewState.Empty
}
} ?: UrlPreviewState.Loading
mutableStateOf(default)
mutableStateOf(
UrlCachedPreviewer.cache.get(url)?.let { it } ?: UrlPreviewState.Loading
)
}
// Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created).
LaunchedEffect(url) {
if (urlPreviewState == UrlPreviewState.Loading) {
launch(Dispatchers.IO) {
UrlCachedPreviewer.previewInfo(
url,
object : IUrlPreviewCallback {
override fun onComplete(urlInfo: UrlInfoItem) {
if (urlInfo.allFetchComplete() && urlInfo.url == url) {
urlPreviewState = UrlPreviewState.Loaded(urlInfo)
} else {
urlPreviewState = UrlPreviewState.Empty
}
}
override fun onFailed(throwable: Throwable) {
urlPreviewState = UrlPreviewState.Error(
context.getString(
R.string.error_parsing_preview_for,
url,
throwable.message
)
)
}
}
)
UrlCachedPreviewer.previewInfo(url) {
urlPreviewState = it
}
}
}
}

View File

@@ -21,6 +21,7 @@ import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.previews.UrlInfoItem
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
@Composable
fun UrlPreviewCard(
@@ -35,7 +36,7 @@ fun UrlPreviewCard(
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
MaterialTheme.colors.subtleBorder,
QuoteBorder
)
) {

View File

@@ -5,8 +5,16 @@ import com.vitorpamplona.amethyst.service.previews.UrlInfoItem
@Immutable
sealed class UrlPreviewState {
@Immutable
object Loading : UrlPreviewState()
@Immutable
class Loaded(val previewInfo: UrlInfoItem) : UrlPreviewState()
@Immutable
object Empty : UrlPreviewState()
@Immutable
class Error(val errorMessage: String) : UrlPreviewState()
}

View File

@@ -131,14 +131,13 @@ fun VideoView(
onDialog: ((Boolean) -> Unit)? = null
) {
val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
val defaultVolume = remember { if (DefaultMutedSetting.value) 0f else 1f }
val media = remember { MediaItem.Builder().setUri(videoUri).build() }
exoPlayerData.exoPlayer.apply {
repeatMode = Player.REPEAT_MODE_ALL
videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT
volume = defaultVolume
volume = if (DefaultMutedSetting.value) 0f else 1f
if (videoUri.startsWith("file") == true) {
setMediaItem(media)
} else {

View File

@@ -48,7 +48,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
@@ -74,8 +73,9 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
import com.vitorpamplona.amethyst.ui.actions.SaveToGallery
import com.vitorpamplona.amethyst.ui.note.BlankNote
import com.vitorpamplona.amethyst.ui.theme.Nip05
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.hashVerified
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
@@ -180,7 +180,7 @@ fun ZoomableContentView(content: ZoomableContent, images: ImmutableList<Zoomable
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
MaterialTheme.colors.subtleBorder,
QuoteBorder
)
@@ -572,7 +572,7 @@ private fun HashVerificationSymbol(verifiedHash: Boolean, modifier: Modifier) {
Icon(
painter = painterResource(R.drawable.ic_verified),
"Hash Verified",
tint = Nip05.copy(0.52f).compositeOver(MaterialTheme.colors.background),
tint = MaterialTheme.colors.hashVerified,
modifier = Modifier.size(30.dp)
)
}

View File

@@ -135,10 +135,17 @@ private fun RowScope.HasNewItemsIcon(
val scope = rememberCoroutineScope()
val size = remember {
if ("Home" == route.base) 25.dp else 23.dp
}
val iconSize = remember {
if ("Home" == route.base) 24.dp else 20.dp
}
BottomIcon(
icon = route.icon,
size = if ("Home" == route.base) 25.dp else 23.dp,
iconSize = if ("Home" == route.base) 24.dp else 20.dp,
size = size,
iconSize = iconSize,
base = route.base,
hasNewItems = hasNewItems,
navEntryState = navEntryState

View File

@@ -80,6 +80,7 @@ import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
@@ -463,7 +464,7 @@ fun SimpleTextSpinner(
imageVector = Icons.Default.ExpandMore,
null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
tint = MaterialTheme.colors.placeholderText
)
}
Box(

View File

@@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.PostButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.placeholderText
class AddBountyAmountViewModel : ViewModel() {
private var account: Account? = null
@@ -56,7 +57,10 @@ class AddBountyAmountViewModel : ViewModel() {
replyTo = listOfNotNull(bounty),
mentions = listOfNotNull(bounty?.author),
tags = listOf("bounty-added-reward"),
wantsToMarkAsSensitive = false
wantsToMarkAsSensitive = false,
replyingTo = null,
root = null,
directMentions = setOf()
)
nextAmount = TextFieldValue("")
@@ -124,7 +128,7 @@ fun AddBountyAmountDialog(bounty: Note, accountViewModel: AccountViewModel, onCl
placeholder = {
Text(
text = "10000, 50000, 5000000",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
singleLine = true

View File

@@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.BadgeCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -132,7 +133,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
Text(
timeAgo(note.createdAt(), context = context),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
maxLines = 1
)
@@ -144,7 +145,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
imageVector = Icons.Default.MoreVert,
null,
modifier = Modifier.size(15.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
tint = MaterialTheme.colors.placeholderText
)
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)

View File

@@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -149,7 +150,7 @@ private fun ChannelRoomCompose(
withStyle(
SpanStyle(
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
fontWeight = FontWeight.Normal
)
) {
@@ -224,7 +225,7 @@ private fun WatchNotificationChanges(
@Composable
fun LoadUser(baseUserHex: String, content: @Composable (User) -> Unit) {
var user by remember(baseUserHex) {
mutableStateOf<User?>(null)
mutableStateOf<User?>(LocalCache.getUserIfExists(baseUserHex))
}
LaunchedEffect(key1 = baseUserHex) {

View File

@@ -26,6 +26,7 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight
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
@@ -38,6 +39,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.compositeOver
@@ -69,6 +71,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeMe
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeThem
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -144,8 +152,8 @@ fun ChatroomMessageCompose(
onClick = { showHiddenNote = true }
)
} else {
val loggedInColors = MaterialTheme.colors.primary.copy(alpha = 0.32f)
val otherColors = MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
val loggedInColors = MaterialTheme.colors.mediumImportanceLink
val otherColors = MaterialTheme.colors.subtleBorder
val defaultBackground = MaterialTheme.colors.background
val backgroundBubbleColor = remember {
@@ -327,7 +335,7 @@ private fun StatusRow(
accountViewModel: AccountViewModel,
onWantsToReply: (Note) -> Unit
) {
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
val grayTint = MaterialTheme.colors.placeholderText
val time = remember { baseNote.createdAt() ?: 0 }
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -361,7 +369,7 @@ fun ChatTimeAgo(time: Long) {
Text(
timeStr,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
fontSize = 12.sp
)
}
@@ -502,10 +510,11 @@ private fun DrawAuthorInfo(
}
}
@Immutable
data class RelayBadgesState(
val shouldDisplayExpandButton: Boolean,
val noteRelays: List<String>,
val noteRelaysSimple: List<String>
val noteRelays: ImmutableList<String>,
val noteRelaysSimple: ImmutableList<String>
)
@OptIn(ExperimentalLayoutApi::class)
@@ -515,8 +524,8 @@ private fun RelayBadges(baseNote: Note) {
val state: RelayBadgesState by remember(noteRelaysState) {
val newShouldDisplayExpandButton = (noteRelaysState?.note?.relays?.size ?: 0) > 3
val noteRelays = noteRelaysState?.note?.relays?.toList() ?: emptyList()
val noteRelaysSimple = noteRelaysState?.note?.relays?.take(3)?.toList() ?: emptyList()
val noteRelays = noteRelaysState?.note?.relays?.toImmutableList() ?: persistentListOf()
val noteRelaysSimple = noteRelaysState?.note?.relays?.take(3)?.toImmutableList() ?: persistentListOf()
mutableStateOf(RelayBadgesState(newShouldDisplayExpandButton, noteRelays, noteRelaysSimple))
}
@@ -544,7 +553,7 @@ private fun RelayBadges(baseNote: Note) {
imageVector = Icons.Default.ChevronRight,
null,
modifier = Modifier.size(15.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
tint = MaterialTheme.colors.placeholderText
)
}
}
@@ -569,10 +578,13 @@ fun RenderRelay(dirtyUrl: String) {
.clickable(onClick = { uri.openUri(website) })
}
val backgroundColor = MaterialTheme.colors.background
val iconModifier = remember(dirtyUrl) {
Modifier
.size(13.dp)
.clip(shape = CircleShape)
.drawBehind { drawRect(backgroundColor) }
}
Box(
@@ -580,11 +592,11 @@ fun RenderRelay(dirtyUrl: String) {
) {
RobohashFallbackAsyncImage(
robot = iconUrl,
robotSize = 13.dp,
robotSize = remember { 13.dp },
model = iconUrl,
contentDescription = stringResource(id = R.string.relay_icon),
colorFilter = RelayIconFilter,
modifier = iconModifier.background(MaterialTheme.colors.background)
modifier = iconModifier
)
}
}

View File

@@ -22,6 +22,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
@@ -46,30 +48,35 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
val scope = rememberCoroutineScope()
var isNew by remember { mutableStateOf(false) }
val defaultBackgroundColor = MaterialTheme.colors.background
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
val newItemColor = MaterialTheme.colors.newItemBackgroundColor
LaunchedEffect(key1 = messageSetCard.createdAt()) {
launch(Dispatchers.IO) {
val newIsNew =
messageSetCard.createdAt() > NotificationCache.load(routeForLastRead)
LaunchedEffect(key1 = messageSetCard) {
scope.launch(Dispatchers.IO) {
val isNew = messageSetCard.createdAt() > NotificationCache.load(routeForLastRead)
NotificationCache.markAsRead(routeForLastRead, messageSetCard.createdAt())
if (newIsNew != isNew) {
isNew = newIsNew
val newBackgroundColor = if (isNew) {
newItemColor.compositeOver(defaultBackgroundColor)
} else {
defaultBackgroundColor
}
if (backgroundColor.value != newBackgroundColor) {
backgroundColor.value = newBackgroundColor
}
}
}
val backgroundColor = if (isNew) {
MaterialTheme.colors.newItemBackgroundColor.compositeOver(MaterialTheme.colors.background)
} else {
MaterialTheme.colors.background
}
val columnModifier = remember(isNew) {
val columnModifier = remember(backgroundColor.value) {
Modifier
.background(backgroundColor)
.drawBehind {
drawRect(
backgroundColor.value
)
}
.padding(
start = 12.dp,
end = 12.dp,

View File

@@ -22,6 +22,7 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bolt
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
@@ -47,17 +48,19 @@ 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.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
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.newItemBackgroundColor
import com.vitorpamplona.amethyst.ui.theme.overPictureBackground
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -91,48 +94,32 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
}
}
val columnModifier = Modifier
.drawBehind {
drawRect(backgroundColor.value)
}
.padding(
start = 12.dp,
end = 12.dp,
top = 10.dp
)
.combinedClickable(
onClick = {
scope.launch {
routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) }
}
},
onLongClick = { popupExpanded = true }
)
.fillMaxWidth()
val zapEvents by remember { derivedStateOf { multiSetCard.zapEvents } }
val boostEvents by remember { derivedStateOf { multiSetCard.boostEvents } }
val likeEvents by remember { derivedStateOf { multiSetCard.likeEvents } }
val hasZapEvents by remember { derivedStateOf { multiSetCard.zapEvents.isNotEmpty() } }
val hasBoostEvents by remember { derivedStateOf { multiSetCard.boostEvents.isNotEmpty() } }
val hasLikeEvents by remember { derivedStateOf { multiSetCard.likeEvents.isNotEmpty() } }
val columnModifier = remember(backgroundColor.value) {
Modifier
.drawBehind {
drawRect(backgroundColor.value)
}
.padding(
start = 12.dp,
end = 12.dp,
top = 10.dp
)
.combinedClickable(
onClick = {
scope.launch {
routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) }
}
},
onLongClick = { popupExpanded = true }
)
.fillMaxWidth()
}
Column(modifier = columnModifier) {
if (hasZapEvents) {
RenderZapGallery(zapEvents, backgroundColor, nav, accountViewModel)
}
Galeries(multiSetCard, backgroundColor, nav, accountViewModel)
if (hasBoostEvents) {
RenderBoostGallery(boostEvents, backgroundColor, nav, accountViewModel)
}
if (hasLikeEvents) {
RenderLikeGallery(likeEvents, backgroundColor, nav, accountViewModel)
}
Row(Modifier.fillMaxWidth()) {
Spacer(modifier = Modifier.width(65.dp))
Row(remember { Modifier.fillMaxWidth() }) {
Spacer(modifier = remember { Modifier.width(65.dp) })
NoteCompose(
baseNote = baseNote,
@@ -149,45 +136,93 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
}
}
@Composable
private fun Galeries(
multiSetCard: MultiSetCard,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
val zapEvents by remember { derivedStateOf { multiSetCard.zapEvents } }
val boostEvents by remember { derivedStateOf { multiSetCard.boostEvents } }
val likeEvents by remember { derivedStateOf { multiSetCard.likeEventsByType } }
val hasZapEvents by remember { derivedStateOf { multiSetCard.zapEvents.isNotEmpty() } }
val hasBoostEvents by remember { derivedStateOf { multiSetCard.boostEvents.isNotEmpty() } }
val hasLikeEvents by remember { derivedStateOf { multiSetCard.likeEvents.isNotEmpty() } }
if (hasZapEvents) {
RenderZapGallery(zapEvents, backgroundColor, nav, accountViewModel)
}
if (hasBoostEvents) {
RenderBoostGallery(boostEvents, backgroundColor, nav, accountViewModel)
}
if (hasLikeEvents) {
likeEvents.forEach {
RenderLikeGallery(it.key, it.value, backgroundColor, nav, accountViewModel)
}
}
}
@Composable
fun RenderLikeGallery(
reactionType: String,
likeEvents: ImmutableList<Note>,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
Row(Modifier.fillMaxWidth()) {
Box(
modifier = remember {
Modifier
.width(55.dp)
.padding(end = 5.dp)
}
) {
Icon(
painter = painterResource(R.drawable.ic_liked),
null,
val isNotEmpty = remember(likeEvents) {
likeEvents.isNotEmpty()
}
val shortReaction by remember {
derivedStateOf {
reactionType.take(2)
}
}
if (isNotEmpty) {
Row(remember { Modifier.fillMaxWidth() }) {
Box(
modifier = remember {
Modifier
.size(16.dp)
.width(55.dp)
.padding(end = 5.dp)
}
) {
val modifier = remember {
Modifier
.align(Alignment.TopEnd)
},
tint = Color.Unspecified
)
}
}
AuthorGallery(likeEvents, backgroundColor, nav, accountViewModel)
when (shortReaction) {
"+" -> Icon(
painter = painterResource(R.drawable.ic_liked),
null,
modifier = remember { modifier.size(18.dp) },
tint = Color.Unspecified
)
"-" -> Text(text = "\uD83D\uDC4E", modifier = modifier)
else -> Text(text = shortReaction, modifier = modifier)
}
}
AuthorGallery(likeEvents, backgroundColor, nav, accountViewModel)
}
}
}
@Composable
fun RenderZapGallery(
zapEvents: ImmutableMap<Note, Note?>,
zapEvents: ImmutableList<CombinedZap>,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
Row(Modifier.fillMaxWidth()) {
Row(remember { Modifier.fillMaxWidth() }) {
Box(
modifier = remember {
Modifier
@@ -249,20 +284,27 @@ fun RenderBoostGallery(
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AuthorGalleryZaps(
authorNotes: ImmutableMap<Note, Note?>,
authorNotes: ImmutableList<CombinedZap>,
backgroundColor: MutableState<Color>,
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
Column(modifier = Modifier.padding(start = 10.dp)) {
Column(modifier = remember { Modifier.padding(start = 10.dp) }) {
FlowRow() {
authorNotes.forEach {
AuthorPictureAndComment(it.key, it.value, backgroundColor, nav, accountViewModel)
AuthorPictureAndComment(it.request, it.response, backgroundColor, nav, accountViewModel)
}
}
}
}
@Immutable
data class ZapAmountCommentNotification(
val user: User?,
val comment: String?,
val amount: String?
)
@Composable
private fun AuthorPictureAndComment(
zapRequest: Note,
@@ -271,7 +313,15 @@ private fun AuthorPictureAndComment(
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
var content by remember { mutableStateOf<Triple<User?, String?, String?>>(Triple(zapRequest.author, null, null)) }
var content by remember {
mutableStateOf(
ZapAmountCommentNotification(
user = zapRequest.author,
comment = null,
amount = null
)
)
}
LaunchedEffect(key1 = zapRequest.idHex, key2 = zapEvent?.idHex) {
launch(Dispatchers.Default) {
@@ -280,27 +330,27 @@ private fun AuthorPictureAndComment(
val amount = (zapEvent?.event as? LnZapEvent)?.amount
if (decryptedContent != null) {
val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey)
content = Triple(newAuthor, decryptedContent.content.ifBlank { null }, showAmountAxis(amount))
content = ZapAmountCommentNotification(newAuthor, decryptedContent.content.ifBlank { null }, showAmountAxis(amount))
} else {
if (!zapRequest.event?.content().isNullOrBlank() || amount != null) {
content = Triple(zapRequest.author, zapRequest.event?.content()?.ifBlank { null }, showAmountAxis(amount))
content = ZapAmountCommentNotification(zapRequest.author, zapRequest.event?.content()?.ifBlank { null }, showAmountAxis(amount))
}
}
}
}
}
content.first?.let {
val route by remember {
derivedStateOf {
"User/${it.pubkeyHex}"
}
val route by remember {
derivedStateOf {
"User/${content.user?.pubkeyHex}"
}
}
content.user?.let { user ->
AuthorPictureAndComment(
author = it,
comment = content.second,
amount = content.third,
author = user,
comment = content.comment,
amount = content.amount,
route = route,
backgroundColor = backgroundColor,
nav = nav,
@@ -309,6 +359,22 @@ private fun AuthorPictureAndComment(
}
}
val amountBoxModifier = Modifier
.fillMaxSize()
.clip(shape = CircleShape)
val textBoxModifier = Modifier.padding(start = 5.dp).fillMaxWidth()
val simpleModifier = Modifier
val size = 35.dp
val sizedModifier = Modifier.size(size)
val bottomPadding1dp = Modifier.padding(bottom = 1.dp)
val commentTextSize = 12.sp
@Composable
private fun AuthorPictureAndComment(
author: User,
@@ -319,8 +385,6 @@ private fun AuthorPictureAndComment(
nav: (String) -> Unit,
accountViewModel: AccountViewModel
) {
val authorPictureModifier = remember { Modifier }
val modifier = remember {
Modifier.clickable {
nav(route)
@@ -331,28 +395,34 @@ private fun AuthorPictureAndComment(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
Box(modifier = remember { Modifier.size(35.dp) }, contentAlignment = Alignment.BottomCenter) {
Box(modifier = sizedModifier, contentAlignment = Alignment.BottomCenter) {
FastNoteAuthorPicture(
author = author,
size = remember { 35.dp },
size = size,
accountViewModel = accountViewModel,
pictureModifier = authorPictureModifier
pictureModifier = simpleModifier
)
amount?.let {
Box(modifier = Modifier.fillMaxSize().clip(shape = CircleShape), contentAlignment = Alignment.BottomCenter) {
Box(
modifier = amountBoxModifier,
contentAlignment = Alignment.BottomCenter
) {
val backgroundColor = MaterialTheme.colors.overPictureBackground
Box(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.background.copy(0.62f)),
modifier = remember {
Modifier
.fillMaxWidth()
.drawBehind { drawRect(backgroundColor) }
},
contentAlignment = Alignment.BottomCenter
) {
Text(
text = it,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.secondaryVariant,
fontSize = 12.sp,
modifier = Modifier.padding(bottom = 1.dp)
fontSize = commentTextSize,
modifier = bottomPadding1dp
)
}
}
@@ -360,12 +430,11 @@ private fun AuthorPictureAndComment(
}
comment?.let {
Spacer(modifier = Modifier.width(5.dp))
TranslatableRichTextViewer(
content = it,
canPreview = true,
tags = remember { ImmutableListOfLists() },
modifier = remember { Modifier.fillMaxWidth() },
modifier = textBoxModifier,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav
@@ -385,14 +454,24 @@ fun AuthorGallery(
Column(modifier = remember { Modifier.padding(start = 10.dp) }) {
FlowRow() {
authorNotes.forEach { note ->
Box(remember { Modifier.size(35.dp) }) {
NotePictureAndComment(note, backgroundColor, nav, accountViewModel)
}
BoxedAuthor(note, backgroundColor, nav, accountViewModel)
}
}
}
}
@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,
@@ -422,17 +501,20 @@ fun FastNoteAuthorPicture(
pictureModifier: Modifier = Modifier,
accountViewModel: AccountViewModel
) {
val userState by author.live().metadata.observeAsState()
val profilePicture by remember(userState) {
derivedStateOf {
userState?.user?.profilePicture()
}
var profilePicture by remember {
mutableStateOf(author.info?.picture)
}
val authorPubKey = remember {
author.pubkeyHex
}
WatchUserMetadata(author) {
if (it.picture != profilePicture) {
profilePicture = it.picture
}
}
UserPicture(
userHex = authorPubKey,
userPicture = profilePicture,
@@ -441,3 +523,15 @@ fun FastNoteAuthorPicture(
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)
}
}
}
}

View File

@@ -14,7 +14,7 @@ import androidx.compose.material.icons.filled.Downloading
import androidx.compose.material.icons.filled.Report
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@@ -34,12 +34,13 @@ import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserMetadata
import com.vitorpamplona.amethyst.service.Nip05Verifier
import com.vitorpamplona.amethyst.ui.theme.Nip05
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Date
@Composable
fun nip05VerificationAsAState(user: UserMetadata, pubkeyHex: String): State<Boolean?> {
fun nip05VerificationAsAState(user: UserMetadata, pubkeyHex: String): MutableState<Boolean?> {
val nip05Verified = remember(user.nip05) {
// starts with null if must verify or already filled in if verified in the last hour
val default = if ((user.nip05LastVerificationTime ?: 0) > (Date().time / 1000 - 60 * 60)) { // 1hour
@@ -131,7 +132,7 @@ fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifie
private fun DisplayNIP05Line(nip05: String, baseUser: User, columnModifier: Modifier = Modifier) {
Column(modifier = columnModifier) {
Row(verticalAlignment = Alignment.CenterVertically) {
val nip05Verified by nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex)
val nip05Verified = nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex)
DisplayNIP05(nip05, nip05Verified)
}
}
@@ -140,7 +141,7 @@ private fun DisplayNIP05Line(nip05: String, baseUser: User, columnModifier: Modi
@Composable
private fun DisplayNIP05(
nip05: String,
nip05Verified: Boolean?
nip05Verified: MutableState<Boolean?>
) {
val uri = LocalUriHandler.current
val (user, domain) = remember(nip05) {
@@ -150,13 +151,26 @@ private fun DisplayNIP05(
if (user != "_") {
Text(
text = AnnotatedString(user),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (nip05Verified == null) {
NIP05VerifiedSymbol(nip05Verified)
ClickableText(
text = AnnotatedString(domain),
onClick = { runCatching { uri.openUri("https://$domain") } },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(0.52f)),
maxLines = 1,
overflow = TextOverflow.Visible
)
}
@Composable
private fun NIP05VerifiedSymbol(nip05Verified: MutableState<Boolean?>) {
if (nip05Verified.value == null) {
Icon(
tint = Color.Yellow,
imageVector = Icons.Default.Downloading,
@@ -165,7 +179,7 @@ private fun DisplayNIP05(
.size(14.dp)
.padding(top = 1.dp)
)
} else if (nip05Verified == true) {
} else if (nip05Verified.value == true) {
Icon(
painter = painterResource(R.drawable.ic_verified),
"NIP-05 Verified",
@@ -184,14 +198,6 @@ private fun DisplayNIP05(
.padding(top = 1.dp)
)
}
ClickableText(
text = AnnotatedString(domain),
onClick = { runCatching { uri.openUri("https://$domain") } },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(0.52f)),
maxLines = 1,
overflow = TextOverflow.Visible
)
}
@Composable

View File

@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -26,8 +25,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
@@ -143,11 +140,13 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Following
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.replyBackground
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentListOf
@@ -635,8 +634,7 @@ private fun RenderNoteRow(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val noteEvent = remember { baseNote.event }
when (noteEvent) {
when (remember { baseNote.event }) {
is AppDefinitionEvent -> {
RenderAppDefinition(baseNote, accountViewModel, nav)
}
@@ -740,7 +738,7 @@ fun routeFor(note: Note, loggedIn: User): String? {
}
@Composable
private fun RenderTextEvent(
fun RenderTextEvent(
note: Note,
makeItShort: Boolean,
canPreview: Boolean,
@@ -748,7 +746,16 @@ private fun RenderTextEvent(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val eventContent = remember(note.event) { accountViewModel.decrypt(note) }
val eventContent = remember(note.event) {
val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null }
val body = accountViewModel.decrypt(note)
if (subject != null) {
"## $subject\n$body"
} else {
body
}
}
if (eventContent != null) {
val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) }
@@ -756,7 +763,7 @@ private fun RenderTextEvent(
if (makeItShort && isAuthorTheLoggedUser) {
Text(
text = eventContent,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
@@ -788,7 +795,7 @@ private fun RenderTextEvent(
}
@Composable
private fun RenderPoll(
fun RenderPoll(
note: Note,
makeItShort: Boolean,
canPreview: Boolean,
@@ -802,7 +809,7 @@ private fun RenderPoll(
if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
Text(
text = eventContent,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
@@ -834,7 +841,7 @@ private fun RenderPoll(
)
}
var hashtags = remember { noteEvent.hashtags().toImmutableList() }
val hashtags = remember { noteEvent.hashtags().toImmutableList() }
DisplayUncitedHashtags(hashtags, eventContent, nav)
}
}
@@ -962,7 +969,7 @@ fun RenderAppDefinition(
if (!website.isNullOrEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
tint = MaterialTheme.colors.placeholderText,
imageVector = Icons.Default.Link,
contentDescription = stringResource(R.string.website),
modifier = Modifier.size(16.dp)
@@ -1058,7 +1065,7 @@ private fun RenderPrivateMessage(
if (makeItShort && isAuthorTheLoggedUser) {
Text(
text = eventContent,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
@@ -1310,17 +1317,8 @@ fun DisplayPeopleList(
)
}
) {
Button(
modifier = Modifier.padding(top = 10.dp),
onClick = { expanded = !expanded },
shape = ButtonBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary.copy(alpha = 0.32f)
.compositeOver(MaterialTheme.colors.background)
),
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
) {
Text(text = stringResource(R.string.show_more), color = Color.White)
ShowMoreButton {
expanded = !expanded
}
}
}
@@ -1527,17 +1525,8 @@ fun PinListHeader(
)
}
) {
Button(
modifier = Modifier.padding(top = 10.dp),
onClick = { expanded = !expanded },
shape = ButtonBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary.copy(alpha = 0.32f)
.compositeOver(MaterialTheme.colors.background)
),
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
) {
Text(text = stringResource(R.string.show_more), color = Color.White)
ShowMoreButton {
expanded = !expanded
}
}
}
@@ -1614,7 +1603,7 @@ private fun RenderReport(
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
MaterialTheme.colors.subtleBorder,
QuoteBorder
),
unPackReply = false,
@@ -1690,18 +1679,22 @@ private fun ReplyNoteComposition(
}
}
val borderColor = MaterialTheme.colors.subtleBorder
NoteCompose(
baseNote = replyingDirectlyTo,
isQuotedNote = true,
modifier = Modifier
.padding(top = 5.dp)
.fillMaxWidth()
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
QuoteBorder
),
modifier = remember {
Modifier
.padding(top = 5.dp)
.fillMaxWidth()
.clip(shape = QuoteBorder)
.border(
1.dp,
borderColor,
QuoteBorder
)
},
unPackReply = false,
makeItShort = true,
parentBackgroundColor = replyBackgroundColor,
@@ -1760,7 +1753,7 @@ private fun FirstUserInfoRow(
Text(
" ${stringResource(id = R.string.boosted)}",
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
} else {
DisplayFollowingHashtagsInPost(eventNote, accountViewModel, nav)
@@ -1780,14 +1773,14 @@ private fun MoreOptionsButton(
var moreActionsExpanded by remember { mutableStateOf(false) }
IconButton(
modifier = Modifier.size(24.dp),
modifier = remember { Modifier.size(24.dp) },
onClick = { moreActionsExpanded = true }
) {
Icon(
imageVector = Icons.Default.MoreVert,
null,
modifier = Modifier.size(15.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
modifier = remember { Modifier.size(15.dp) },
tint = MaterialTheme.colors.placeholderText
)
NoteDropDownMenu(
@@ -1815,7 +1808,7 @@ fun TimeAgo(time: Long) {
Text(
timeStr,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
maxLines = 1
)
}
@@ -1857,7 +1850,7 @@ private fun DrawAuthorImages(baseNote: Note, accountViewModel: AccountViewModel,
@Composable
fun LoadChannel(baseChannelHex: String, content: @Composable (Channel) -> Unit) {
var channel by remember(baseChannelHex) {
mutableStateOf<Channel?>(null)
mutableStateOf<Channel?>(LocalCache.getChannelIfExists(baseChannelHex))
}
LaunchedEffect(key1 = baseChannelHex) {
@@ -1878,11 +1871,19 @@ private fun ChannelNotePicture(baseChannel: Channel) {
val channelState by baseChannel.live.observeAsState()
val channel = remember(channelState) { channelState?.channel } ?: return
val backgroundColor = MaterialTheme.colors.background
val modifier = remember {
Modifier
.width(30.dp)
.height(30.dp)
.clip(shape = CircleShape)
.drawBehind { drawRect(backgroundColor) }
.border(
2.dp,
backgroundColor,
CircleShape
)
}
val boxModifier = remember {
@@ -1901,12 +1902,6 @@ private fun ChannelNotePicture(baseChannel: Channel) {
model = model,
contentDescription = stringResource(R.string.group_picture),
modifier = modifier
.background(MaterialTheme.colors.background)
.border(
2.dp,
MaterialTheme.colors.background,
CircleShape
)
)
}
}
@@ -2065,9 +2060,13 @@ fun DisplayUncitedHashtags(
eventContent: String,
nav: (String) -> Unit
) {
if (hashtags.isNotEmpty()) {
val hasHashtags = remember {
hashtags.isNotEmpty()
}
if (hasHashtags) {
FlowRow(
modifier = Modifier.padding(top = 5.dp)
modifier = remember { Modifier.padding(top = 5.dp) }
) {
hashtags.forEach { hashtag ->
if (!eventContent.contains(hashtag, true)) {
@@ -2184,13 +2183,13 @@ private fun RenderPledgeAmount(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
tint = MaterialTheme.colors.placeholderText
)
}
Text(
text = reward,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
}
@@ -2228,7 +2227,7 @@ fun BadgeDisplay(baseNote: Note) {
.clip(shape = CutCornerShape(20, 20, 20, 20))
.border(
5.dp,
MaterialTheme.colors.primary.copy(alpha = 0.32f),
MaterialTheme.colors.mediumImportanceLink,
CutCornerShape(20)
)
.background(backgroundFromImage.first)
@@ -2434,7 +2433,7 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, accountViewModel: AccountViewMo
it.first.role?.let {
Text(
text = it.capitalize(Locale.ROOT),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
maxLines = 1
)
}
@@ -2474,7 +2473,7 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, accountView
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
MaterialTheme.colors.subtleBorder,
QuoteBorder
)
) {
@@ -2577,7 +2576,7 @@ private fun RelayBadges(baseNote: Note) {
}
}
Spacer(Modifier.height(10.dp))
Spacer(remember { Modifier.height(10.dp) })
if (expanded) {
VerticalRelayPanelWithFlow(lazyRelayList)
@@ -2621,30 +2620,26 @@ private fun VerticalRelayPanelWithFlow(
}
}
val showMoreRelaysButtonIconButtonModifier = Modifier.size(24.dp)
val showMoreRelaysButtonIconModifier = Modifier.size(15.dp)
val showMoreRelaysButtonBoxModifer = Modifier.fillMaxWidth().height(25.dp)
@Composable
private fun ShowMoreRelaysButton(onClick: () -> Unit) {
val boxModifier = remember {
Modifier
.fillMaxWidth()
.height(25.dp)
}
val iconButtonModifier = remember { Modifier.size(24.dp) }
val iconModifier = remember { Modifier.size(15.dp) }
Row(
boxModifier,
modifier = showMoreRelaysButtonBoxModifer,
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Top
) {
IconButton(
modifier = iconButtonModifier,
modifier = showMoreRelaysButtonIconButtonModifier,
onClick = onClick
) {
Icon(
imageVector = Icons.Default.ExpandMore,
null,
modifier = iconModifier,
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
modifier = showMoreRelaysButtonIconModifier,
tint = MaterialTheme.colors.placeholderText
)
}
}
@@ -2679,17 +2674,20 @@ fun NoteAuthorPicture(
}
if (author == null) {
val backgroundColor = MaterialTheme.colors.background
val nullModifier = remember {
modifier
.size(size)
.clip(shape = CircleShape)
.drawBehind { drawRect(backgroundColor) }
}
RobohashAsyncImage(
robot = "authornotfound",
robotSize = size,
contentDescription = stringResource(R.string.unknown_author),
modifier = nullModifier.background(MaterialTheme.colors.background)
modifier = nullModifier
)
} else {
UserPicture(author!!, size, accountViewModel, modifier, onClick)
@@ -2764,6 +2762,8 @@ fun UserPicture(
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel
) {
val backgroundColor = MaterialTheme.colors.background
val myBoxModifier = remember {
Modifier.size(size)
}
@@ -2772,6 +2772,7 @@ fun UserPicture(
modifier
.size(size)
.clip(shape = CircleShape)
.drawBehind { drawRect(backgroundColor) }
}
val myIconSize = remember(size) { size.div(3.5f) }
@@ -2783,7 +2784,7 @@ fun UserPicture(
ResizeImage(userPicture, size)
},
contentDescription = stringResource(id = R.string.profile_image),
modifier = myImageModifier.background(MaterialTheme.colors.background)
modifier = myImageModifier
)
ObserveAndDisplayFollowingMark(userHex, myIconSize, accountViewModel)
@@ -2820,14 +2821,17 @@ private fun FollowingIcon(iconSize: Dp) {
}
Box(myIconBoxModifier, contentAlignment = Alignment.Center) {
val backgroundColor = MaterialTheme.colors.background
val myIconBackgroundModifier = remember {
Modifier
.clip(CircleShape)
.fillMaxSize(0.6f)
.drawBehind { drawRect(backgroundColor) }
}
Box(
myIconBackgroundModifier.background(MaterialTheme.colors.background)
myIconBackgroundModifier
)
val myIconModifier = remember {

View File

@@ -45,7 +45,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalClipboardManager
@@ -69,6 +68,7 @@ import com.vitorpamplona.amethyst.ui.components.SelectTextDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
import com.vitorpamplona.amethyst.ui.theme.WarningColor
import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -112,12 +112,12 @@ private fun VerticalDivider(color: Color) =
@Composable
fun LongPressToQuickAction(baseNote: Note, accountViewModel: AccountViewModel, content: @Composable (() -> Unit) -> Unit) {
var popupExpanded by remember { mutableStateOf(false) }
val showPopup = remember { { popupExpanded = true } }
var popupExpanded = remember { mutableStateOf(false) }
val showPopup = remember(popupExpanded) { { popupExpanded.value = true } }
content(showPopup)
NoteQuickActionMenu(baseNote, popupExpanded, { popupExpanded = false }, accountViewModel)
NoteQuickActionMenu(baseNote, popupExpanded.value, { popupExpanded.value = false }, accountViewModel)
}
@Composable
@@ -137,7 +137,7 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
val backgroundColor = if (MaterialTheme.colors.isLight) {
MaterialTheme.colors.primary
} else {
MaterialTheme.colors.primary.copy(alpha = 0.32f).compositeOver(MaterialTheme.colors.background)
MaterialTheme.colors.secondaryButtonBackground
}
val showToast = { stringResource: Int ->

View File

@@ -33,6 +33,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
@@ -124,7 +126,7 @@ private fun OptionNote(
val color = if (poolOption.consensusThreadhold) {
Color.Green.copy(alpha = 0.32f)
} else {
MaterialTheme.colors.primary.copy(alpha = 0.32f)
MaterialTheme.colors.mediumImportanceLink
}
ZapVote(
@@ -411,7 +413,7 @@ fun ZapVote(
imageVector = Icons.Outlined.Bolt,
contentDescription = stringResource(id = R.string.zaps),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
tint = MaterialTheme.colors.placeholderText
)
} else {
Spacer(Modifier.width(3.dp))
@@ -429,7 +431,7 @@ fun ZapVote(
Text(
showAmount(poolOption.zappedValue),
fontSize = 14.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
modifier = modifier
)
}

View File

@@ -54,6 +54,7 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup
@@ -64,9 +65,11 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.screen.CombinedZap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.CoroutineScope
@@ -78,9 +81,9 @@ import kotlin.math.roundToInt
@Composable
fun ReactionsRow(baseNote: Note, showReactionDetail: Boolean, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
val grayTint = MaterialTheme.colors.placeholderText
var wantsToSeeReactions = remember {
val wantsToSeeReactions = remember {
mutableStateOf<Boolean>(false)
}
@@ -187,9 +190,9 @@ private fun ReactionDetailGallery(
if (hasReactions) {
Row(verticalAlignment = CenterVertically, modifier = Modifier.padding(start = 10.dp, top = 5.dp)) {
Column() {
val zapEvents by remember(zapsState) { derivedStateOf { baseNote.zaps.toImmutableMap() } }
val zapEvents by remember(zapsState) { derivedStateOf { baseNote.zaps.mapNotNull { it.value?.let { zapEvent -> CombinedZap(it.key, zapEvent) } }.toImmutableList() } }
val boostEvents by remember(boostsState) { derivedStateOf { baseNote.boosts.toImmutableList() } }
val likeEvents by remember(reactionsState) { derivedStateOf { baseNote.reactions.toImmutableList() } }
val likeEvents by remember(reactionsState) { derivedStateOf { baseNote.reactions.toImmutableMap() } }
val hasZapEvents by remember(zapsState) { derivedStateOf { baseNote.zaps.isNotEmpty() } }
val hasBoostEvents by remember(boostsState) { derivedStateOf { baseNote.boosts.isNotEmpty() } }
@@ -214,12 +217,16 @@ private fun ReactionDetailGallery(
}
if (hasLikeEvents) {
RenderLikeGallery(
likeEvents,
backgroundColor,
nav,
accountViewModel
)
likeEvents.forEach {
val reactions = remember(it.value) { it.value.toImmutableList() }
RenderLikeGallery(
it.key,
reactions,
backgroundColor,
nav,
accountViewModel
)
}
}
}
}
@@ -428,13 +435,15 @@ fun BoostText(baseNote: Note, grayTint: Color) {
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LikeReaction(
baseNote: Note,
grayTint: Color,
accountViewModel: AccountViewModel,
iconSize: Dp = 20.dp,
heartSize: Dp = 16.dp
heartSize: Dp = 16.dp,
iconFontSize: TextUnit = 14.sp
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -443,47 +452,75 @@ fun LikeReaction(
Modifier.size(iconSize)
}
IconButton(
modifier = iconButtonModifier,
onClick = {
if (accountViewModel.isWriteable()) {
scope.launch(Dispatchers.IO) {
if (accountViewModel.hasReactedTo(baseNote)) {
accountViewModel.deleteReactionTo(baseNote)
} else {
accountViewModel.reactTo(baseNote)
var wantsToChangeReactionSymbol by remember { mutableStateOf(false) }
var wantsToReact by remember { mutableStateOf(false) }
Row(
verticalAlignment = CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = iconButtonModifier.combinedClickable(
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = 24.dp),
onClick = {
likeClick(
baseNote,
accountViewModel,
scope,
context,
onMultipleChoices = {
wantsToReact = true
}
}
} else {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_like_posts),
Toast.LENGTH_SHORT
).show()
}
)
},
onLongClick = {
wantsToChangeReactionSymbol = true
}
}
)
) {
LikeIcon(baseNote, heartSize, grayTint, accountViewModel.userProfile())
LikeIcon(baseNote, iconFontSize, heartSize, grayTint, accountViewModel.userProfile())
if (wantsToChangeReactionSymbol) {
UpdateReactionTypeDialog({ wantsToChangeReactionSymbol = false }, accountViewModel = accountViewModel)
}
if (wantsToReact) {
ReactionChoicePopup(
baseNote,
accountViewModel,
onDismiss = {
wantsToReact = false
},
onChangeAmount = {
wantsToReact = false
wantsToChangeReactionSymbol = true
}
)
}
}
LikeText(baseNote, grayTint)
}
@Composable
fun LikeIcon(baseNote: Note, iconSize: Dp = 20.dp, grayTint: Color, loggedIn: User) {
fun LikeIcon(
baseNote: Note,
iconFontSize: TextUnit = 14.sp,
iconSize: Dp = 20.dp,
grayTint: Color,
loggedIn: User
) {
val reactionsState by baseNote.live().reactions.observeAsState()
var wasReactedByLoggedIn by remember(reactionsState) {
mutableStateOf(false)
var reactionType by remember(baseNote) {
mutableStateOf<String?>(null)
}
LaunchedEffect(key1 = reactionsState) {
launch(Dispatchers.Default) {
val newWasReactedByLoggedIn = reactionsState?.note?.isReactedBy(loggedIn) == true
if (wasReactedByLoggedIn != newWasReactedByLoggedIn) {
wasReactedByLoggedIn = newWasReactedByLoggedIn
val newReactionType = reactionsState?.note?.isReactedBy(loggedIn)
if (reactionType != newReactionType) {
reactionType = newReactionType?.take(2)
}
}
}
@@ -492,13 +529,19 @@ fun LikeIcon(baseNote: Note, iconSize: Dp = 20.dp, grayTint: Color, loggedIn: Us
Modifier.size(iconSize)
}
if (wasReactedByLoggedIn) {
Icon(
painter = painterResource(R.drawable.ic_liked),
null,
modifier = iconModifier,
tint = Color.Unspecified
)
if (reactionType != null) {
when (reactionType) {
"+" -> {
Icon(
painter = painterResource(R.drawable.ic_liked),
null,
modifier = iconModifier,
tint = Color.Unspecified
)
}
"-" -> Text(text = "\uD83D\uDC4E", fontSize = iconFontSize)
else -> Text(text = reactionType!!, fontSize = iconFontSize)
}
} else {
Icon(
painter = painterResource(R.drawable.ic_like),
@@ -519,7 +562,7 @@ fun LikeText(baseNote: Note, grayTint: Color) {
LaunchedEffect(key1 = reactionsState) {
launch(Dispatchers.Default) {
val newReactionsCount = " " + showCount(reactionsState?.note?.reactions?.size)
val newReactionsCount = " " + showCount(reactionsState?.note?.countReactions())
if (reactionsCount != newReactionsCount) {
reactionsCount = newReactionsCount
}
@@ -533,6 +576,47 @@ fun LikeText(baseNote: Note, grayTint: Color) {
)
}
private fun likeClick(
baseNote: Note,
accountViewModel: AccountViewModel,
scope: CoroutineScope,
context: Context,
onMultipleChoices: () -> Unit
) {
if (accountViewModel.account.reactionChoices.isEmpty()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.no_reaction_type_setup_long_press_to_change),
Toast.LENGTH_SHORT
)
.show()
}
} else if (!accountViewModel.isWriteable()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps),
Toast.LENGTH_SHORT
)
.show()
}
} else if (accountViewModel.account.reactionChoices.size == 1) {
scope.launch(Dispatchers.IO) {
val reaction = accountViewModel.account.reactionChoices.first()
if (accountViewModel.hasReactedTo(baseNote, reaction)) {
accountViewModel.deleteReactionTo(baseNote, reaction)
} else {
accountViewModel.reactTo(baseNote, reaction)
}
}
} else if (accountViewModel.account.reactionChoices.size > 1) {
onMultipleChoices()
}
}
@Composable
@OptIn(ExperimentalFoundationApi::class)
fun ZapReaction(
@@ -862,6 +946,95 @@ private fun BoostTypeChoicePopup(baseNote: Note, accountViewModel: AccountViewMo
}
}
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
@Composable
fun ReactionChoicePopup(
baseNote: Note,
accountViewModel: AccountViewModel,
onDismiss: () -> Unit,
onChangeAmount: () -> Unit
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val scope = rememberCoroutineScope()
val toRemove = remember {
baseNote.reactedBy(account.userProfile()).toSet()
}
Popup(
alignment = Alignment.BottomCenter,
offset = IntOffset(0, -50),
onDismissRequest = { onDismiss() }
) {
FlowRow(horizontalArrangement = Arrangement.Center) {
account.reactionChoices.forEach { reactionType ->
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {
scope.launch(Dispatchers.IO) {
accountViewModel.reactToOrDelete(
baseNote,
reactionType
)
onDismiss()
}
},
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
) {
val thisModifier = remember(reactionType) {
Modifier.combinedClickable(
onClick = {
scope.launch(Dispatchers.IO) {
accountViewModel.reactToOrDelete(
baseNote,
reactionType
)
onDismiss()
}
},
onLongClick = {
onChangeAmount()
}
)
}
val removeSymbol = remember(reactionType) {
if (reactionType in toRemove) {
""
} else {
""
}
}
when (reactionType) {
"+" -> {
Icon(
painter = painterResource(R.drawable.ic_liked),
null,
modifier = remember { thisModifier.size(16.dp) },
tint = Color.White
)
Text(text = removeSymbol, color = Color.White, textAlign = TextAlign.Center, modifier = thisModifier)
}
"-" -> Text(text = "\uD83D\uDC4E$removeSymbol", color = Color.White, textAlign = TextAlign.Center, modifier = thisModifier)
else -> Text(
"$reactionType$removeSymbol",
color = Color.White,
textAlign = TextAlign.Center,
modifier = thisModifier
)
}
}
}
}
}
}
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
@Composable
fun ZapAmountChoicePopup(

View File

@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.RelayInfo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@@ -67,7 +68,7 @@ fun RelayCompose(
Text(
"${relay.counter} ${stringResource(R.string.posts_received)}",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)

View File

@@ -21,6 +21,8 @@ import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
@@ -69,7 +71,7 @@ private fun ReplyInformation(
Text(
stringResource(R.string.replying_to),
fontSize = 13.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
repliesToDisplay.forEachIndexed { idx, user ->
@@ -80,13 +82,13 @@ private fun ReplyInformation(
Text(
", ",
fontSize = 13.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
} else if (idx < repliesToDisplay.size - 1) {
Text(
stringResource(R.string.and),
fontSize = 13.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
}
} else {
@@ -94,25 +96,25 @@ private fun ReplyInformation(
Text(
", ",
fontSize = 13.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
} else if (idx < repliesToDisplay.size) {
Text(
stringResource(R.string.and),
fontSize = 13.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
ClickableText(
AnnotatedString("${sortedMentions.size - 2}"),
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(alpha = 0.52f), fontSize = 13.sp),
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.lessImportantLink, fontSize = 13.sp),
onClick = { expanded = true }
)
Text(
" ${stringResource(R.string.others)}",
fontSize = 13.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
}
}
@@ -195,12 +197,12 @@ fun ReplyInformationChannel(
Text(
stringResource(R.string.in_channel),
fontSize = 13.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
ClickableText(
text = channelName,
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(alpha = 0.52f), fontSize = 13.sp),
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.lessImportantLink, fontSize = 13.sp),
onClick = { onChannelTagClick(channel) }
)
@@ -209,7 +211,7 @@ fun ReplyInformationChannel(
Text(
stringResource(id = R.string.replying_to),
fontSize = 13.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
mentions.forEachIndexed { idx, user ->
@@ -219,13 +221,13 @@ fun ReplyInformationChannel(
Text(
", ",
fontSize = 13.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
} else if (idx < mentions.size - 1) {
Text(
" ${stringResource(id = R.string.add)} ",
" ${stringResource(id = R.string.and)} ",
fontSize = 13.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
}
}
@@ -246,7 +248,7 @@ private fun ReplyInfoMention(
clickablePart = remember(innerUserState) { "$prefix${innerUserState?.user?.toBestDisplayName()}" },
tags = remember(innerUserState) { innerUserState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() },
style = LocalTextStyle.current.copy(
color = MaterialTheme.colors.primary.copy(alpha = 0.52f),
color = MaterialTheme.colors.lessImportantLink,
fontSize = 13.sp
),
onClick = { onUserTagClick(user) }

View File

@@ -0,0 +1,231 @@
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.SaveButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.placeholderText
class UpdateReactionTypeViewModel(val account: Account) : ViewModel() {
var nextChoice by mutableStateOf(TextFieldValue(""))
var reactionSet by mutableStateOf(listOf<String>())
fun load() {
this.reactionSet = account.reactionChoices
}
fun toListOfChoices(commaSeparatedAmounts: String): List<Long> {
return commaSeparatedAmounts.split(",").map { it.trim().toLongOrNull() ?: 0 }
}
fun addChoice() {
val newValue = nextChoice.text.trim().take(2)
reactionSet = reactionSet + newValue
nextChoice = TextFieldValue("")
}
fun removeChoice(reaction: String) {
reactionSet = reactionSet - reaction
}
fun sendPost() {
account.changeReactionTypes(reactionSet)
nextChoice = TextFieldValue("")
}
fun cancel() {
nextChoice = TextFieldValue("")
}
fun hasChanged(): Boolean {
return reactionSet != account.reactionChoices
}
class Factory(val account: Account) : ViewModelProvider.Factory {
override fun <UpdateReactionTypeViewModel : ViewModel> create(modelClass: Class<UpdateReactionTypeViewModel>): UpdateReactionTypeViewModel {
return UpdateReactionTypeViewModel(account) as UpdateReactionTypeViewModel
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun UpdateReactionTypeDialog(onClose: () -> Unit, nip47uri: String? = null, accountViewModel: AccountViewModel) {
val postViewModel: UpdateReactionTypeViewModel = viewModel(
key = accountViewModel.userProfile().pubkeyHex,
factory = UpdateReactionTypeViewModel.Factory(accountViewModel.account)
)
LaunchedEffect(accountViewModel) {
postViewModel.load()
}
Dialog(
onDismissRequest = { onClose() },
properties = DialogProperties(
usePlatformDefaultWidth = false,
dismissOnClickOutside = false,
decorFitsSystemWindows = false
)
) {
Surface(
modifier = Modifier
.fillMaxWidth()
) {
Column(modifier = Modifier.padding(10.dp).imePadding()) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
postViewModel.cancel()
onClose()
})
SaveButton(
onPost = {
postViewModel.sendPost()
onClose()
},
isActive = postViewModel.hasChanged()
)
}
Spacer(modifier = Modifier.height(10.dp))
Row(
modifier = Modifier
.fillMaxWidth()
) {
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
Row(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.animateContentSize()) {
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
postViewModel.reactionSet.forEach { reactionType ->
Button(
modifier = Modifier.padding(horizontal = 3.dp),
shape = ButtonBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
),
onClick = {
postViewModel.removeChoice(reactionType)
}
) {
when (reactionType) {
"+" -> {
Icon(
painter = painterResource(R.drawable.ic_liked),
null,
modifier = remember { Modifier.size(16.dp) },
tint = Color.White
)
Text(text = "", color = Color.White, textAlign = TextAlign.Center)
}
"-" -> Text(text = "\uD83D\uDC4E", color = Color.White, textAlign = TextAlign.Center)
else -> Text(text = "$reactionType", color = Color.White, textAlign = TextAlign.Center)
}
}
}
}
}
}
Spacer(modifier = Modifier.height(10.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 5.dp),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
label = { Text(text = stringResource(R.string.new_reaction_symbol)) },
value = postViewModel.nextChoice,
onValueChange = {
postViewModel.nextChoice = it
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.None,
keyboardType = KeyboardType.Text
),
placeholder = {
Text(
text = "\uD83D\uDCAF, \uD83C\uDF89, \uD83D\uDC4E",
color = MaterialTheme.colors.placeholderText
)
},
singleLine = true,
modifier = Modifier
.padding(end = 10.dp)
.weight(1f)
)
Button(
onClick = { postViewModel.addChoice() },
shape = ButtonBorder,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
) {
Text(text = stringResource(R.string.add), color = Color.White)
}
}
}
}
}
}
}
}

View File

@@ -78,6 +78,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.getFragmentActivity
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -324,7 +325,7 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, nip47uri: String? = null, account
placeholder = {
Text(
text = "100, 1000, 5000",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
singleLine = true,
@@ -413,7 +414,7 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, nip47uri: String? = null, account
Text(
stringResource(id = R.string.wallet_connect_service_explainer),
Modifier.weight(1f),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
fontSize = 14.sp
)
}
@@ -452,7 +453,7 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, nip47uri: String? = null, account
placeholder = {
Text(
text = "npub, hex",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
singleLine = true,
@@ -474,7 +475,7 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, nip47uri: String? = null, account
placeholder = {
Text(
text = "wss://relay.server.com",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
maxLines = 1
)
},
@@ -518,7 +519,7 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, nip47uri: String? = null, account
placeholder = {
Text(
text = stringResource(R.string.wallet_connect_service_secret_placeholder),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
trailingIcon = {

View File

@@ -48,6 +48,7 @@ import com.vitorpamplona.amethyst.ui.components.BundledInsert
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.RoyalBlue
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
@@ -80,7 +81,7 @@ fun UserReactionsRow(
imageVector = Icons.Default.ExpandMore,
null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
tint = MaterialTheme.colors.placeholderText
)
}

View File

@@ -14,6 +14,7 @@ import com.vitorpamplona.amethyst.model.User
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.placeholderText
@Composable
fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier) {
@@ -53,7 +54,7 @@ private fun UserNameDisplay(
CreateTextWithEmoji(
text = remember { "@$bestUserName" },
tags = tags,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = modifier

View File

@@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -148,7 +149,7 @@ fun ZapCustomDialog(onClose: () -> Unit, accountViewModel: AccountViewModel, bas
placeholder = {
Text(
text = "100, 1000, 5000",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
singleLine = true,
@@ -199,7 +200,7 @@ fun ZapCustomDialog(onClose: () -> Unit, accountViewModel: AccountViewModel, bas
placeholder = {
Text(
text = stringResource(id = R.string.custom_zaps_add_a_message_example),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
singleLine = true,

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.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -225,7 +226,7 @@ fun AboutDisplay(baseAuthor: User) {
Text(
userAboutMe,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)

View File

@@ -6,7 +6,8 @@ import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
@Immutable
abstract class Card() {
@@ -33,8 +34,8 @@ class NoteCard(val note: Note) : Card() {
}
@Immutable
class ZapUserSetCard(val user: User, val zapEvents: ImmutableMap<Note, Note>) : Card() {
val createdAt = zapEvents.maxOf { it.value.createdAt() ?: 0 }
class ZapUserSetCard(val user: User, val zapEvents: ImmutableList<CombinedZap>) : Card() {
val createdAt = zapEvents.maxOf { it.createdAt() ?: 0 }
override fun createdAt(): Long {
return createdAt
}
@@ -42,19 +43,28 @@ class ZapUserSetCard(val user: User, val zapEvents: ImmutableMap<Note, Note>) :
}
@Immutable
class MultiSetCard(val note: Note, val boostEvents: ImmutableList<Note>, val likeEvents: ImmutableList<Note>, val zapEvents: ImmutableMap<Note, Note>) : Card() {
class MultiSetCard(
val note: Note,
val boostEvents: ImmutableList<Note>,
val likeEvents: ImmutableList<Note>,
val zapEvents: ImmutableList<CombinedZap>
) : Card() {
val maxCreatedAt = maxOf(
zapEvents.maxOfOrNull { it.value.createdAt() ?: 0 } ?: 0,
zapEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0,
likeEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0,
boostEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0
)
val minCreatedAt = minOf(
zapEvents.minOfOrNull { it.value.createdAt() ?: Long.MAX_VALUE } ?: Long.MAX_VALUE,
zapEvents.minOfOrNull { it.createdAt() ?: Long.MAX_VALUE } ?: Long.MAX_VALUE,
likeEvents.minOfOrNull { it.createdAt() ?: Long.MAX_VALUE } ?: Long.MAX_VALUE,
boostEvents.minOfOrNull { it.createdAt() ?: Long.MAX_VALUE } ?: Long.MAX_VALUE
)
val likeEventsByType = likeEvents.groupBy { it.event?.content() ?: "+" }.mapValues {
it.value.toImmutableList()
}.toImmutableMap()
override fun createdAt(): Long {
return maxCreatedAt
}

View File

@@ -2,11 +2,11 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
@@ -153,6 +153,7 @@ fun RenderCardFeed(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun FeedLoaded(
state: CardFeedState.Loaded,
@@ -161,12 +162,6 @@ private fun FeedLoaded(
nav: (String) -> Unit,
routeForLastRead: String
) {
val defaultModifier = remember {
Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 100.dp)
}
LazyColumn(
modifier = remember { Modifier.fillMaxSize() },
contentPadding = remember {
@@ -178,6 +173,11 @@ private fun FeedLoaded(
state = listState
) {
itemsIndexed(state.feed.value, key = { _, item -> item.id() }) { _, item ->
val defaultModifier = remember {
Modifier
.fillMaxWidth().animateItemPlacement()
}
Row(defaultModifier) {
RenderCardItem(item, accountViewModel, nav, routeForLastRead)
}

View File

@@ -1,6 +1,7 @@
package com.vitorpamplona.amethyst.ui.screen
import android.util.Log
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
@@ -25,7 +26,6 @@ import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -132,8 +132,8 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
}
// val reactionCards = reactionsPerEvent.map { LikeSetCard(it.key, it.value) }
val zapsPerUser = mutableMapOf<User, MutableMap<Note, Note>>()
val zapsPerEvent = mutableMapOf<Note, MutableMap<Note, Note>>()
val zapsPerUser = mutableMapOf<User, MutableList<CombinedZap>>()
val zapsPerEvent = mutableMapOf<Note, MutableList<CombinedZap>>()
notes
.filter { it.event is LnZapEvent }
.forEach { zapEvent ->
@@ -143,27 +143,23 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
if (zapRequest != null) {
// var newZapRequestEvent = LocalCache.checkPrivateZap(zapRequest.event as Event)
// zapRequest.event = newZapRequestEvent
zapsPerEvent.getOrPut(zappedPost, { mutableMapOf() }).put(zapRequest, zapEvent)
zapsPerEvent.getOrPut(zappedPost, { mutableListOf() }).add(CombinedZap(zapRequest, zapEvent))
}
} else {
val event = (zapEvent.event as LnZapEvent)
val author = event.zappedAuthor().mapNotNull {
LocalCache.checkGetOrCreateUser(
it
)
}.firstOrNull()
val author = event.zappedAuthor().firstNotNullOfOrNull {
LocalCache.users[it] // don't create user if it doesn't exist
}
if (author != null) {
val zapRequest = author.zaps.filter { it.value == zapEvent }.keys.firstOrNull()
if (zapRequest != null) {
zapsPerUser.getOrPut(author, { mutableMapOf() })
.put(zapRequest, zapEvent)
zapsPerUser.getOrPut(author, { mutableListOf() })
.add(CombinedZap(zapRequest, zapEvent))
}
}
}
}
// val zapCards = zapsPerEvent.map { ZapSetCard(it.key, it.value) }
val boostsPerEvent = mutableMapOf<Note, MutableList<Note>>()
notes
.filter { it.event is RepostEvent }
@@ -178,9 +174,9 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
val multiCards = allBaseNotes.map { baseNote ->
val boostsInCard = boostsPerEvent[baseNote] ?: emptyList()
val reactionsInCard = reactionsPerEvent[baseNote] ?: emptyList()
val zapsInCard = zapsPerEvent[baseNote] ?: emptyMap()
val zapsInCard = zapsPerEvent[baseNote] ?: emptyList()
val singleList = (boostsInCard + zapsInCard.values + reactionsInCard)
val singleList = (boostsInCard + zapsInCard.map { it.response } + reactionsInCard)
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
.reversed()
@@ -189,7 +185,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
baseNote,
boostsInCard.filter { it in chunk }.toImmutableList(),
reactionsInCard.filter { it in chunk }.toImmutableList(),
zapsInCard.filter { it.value in chunk }.toImmutableMap()
zapsInCard.filter { it.response in chunk }.toImmutableList()
)
}
}.flatten()
@@ -197,7 +193,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
val userZaps = zapsPerUser.map {
ZapUserSetCard(
it.key,
it.value.toImmutableMap()
it.value.toImmutableList()
)
}
@@ -346,3 +342,9 @@ fun <T> equalImmutableLists(list1: ImmutableList<T>, list2: ImmutableList<T>): B
}
return true
}
@Immutable
data class CombinedZap(val request: Note, val response: Note) {
fun createdAt() = response.createdAt()
fun idHex() = response.idHex
}

View File

@@ -2,12 +2,12 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
@@ -168,6 +168,7 @@ private fun WatchScrollToTop(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun FeedLoaded(
state: FeedState.Loaded,
@@ -188,7 +189,12 @@ private fun FeedLoaded(
state = listState
) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
Row(Modifier.fillMaxWidth().defaultMinSize(minHeight = 75.dp)) {
val defaultModifier = remember {
Modifier
.fillMaxWidth().animateItemPlacement()
}
Row(defaultModifier) {
NoteCompose(
item,
routeForLastRead = routeForLastRead,

View File

@@ -22,6 +22,7 @@ import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.SnackbarDefaults.backgroundColor
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
@@ -45,8 +46,10 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R
@@ -60,18 +63,13 @@ import com.vitorpamplona.amethyst.service.model.PeopleListEvent
import com.vitorpamplona.amethyst.service.model.PinListEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
import com.vitorpamplona.amethyst.service.model.RelaySetEvent
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.note.*
import com.vitorpamplona.amethyst.ui.note.BadgeDisplay
import com.vitorpamplona.amethyst.ui.note.BlankNote
import com.vitorpamplona.amethyst.ui.note.DisplayFollowingHashtagsInPost
import com.vitorpamplona.amethyst.ui.note.DisplayPoW
import com.vitorpamplona.amethyst.ui.note.DisplayReward
import com.vitorpamplona.amethyst.ui.note.DisplayUncitedHashtags
import com.vitorpamplona.amethyst.ui.note.HiddenNote
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
import com.vitorpamplona.amethyst.ui.note.NoteCompose
@@ -81,8 +79,9 @@ import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
import com.vitorpamplona.amethyst.ui.note.timeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.selectedNote
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.delay
@@ -137,16 +136,18 @@ fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: A
) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { index, item ->
if (index == 0) {
NoteMaster(
item,
modifier = Modifier.drawReplyLevel(
item.replyLevel(),
MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
if (item.idHex == noteId) MaterialTheme.colors.primary.copy(alpha = 0.52f) else MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
),
accountViewModel = accountViewModel,
nav = nav
)
ProvideTextStyle(TextStyle(fontSize = 16.sp, lineHeight = 1.20.em)) {
NoteMaster(
item,
modifier = Modifier.drawReplyLevel(
item.replyLevel(),
MaterialTheme.colors.placeholderText,
if (item.idHex == noteId) MaterialTheme.colors.lessImportantLink else MaterialTheme.colors.placeholderText
),
accountViewModel = accountViewModel,
nav = nav
)
}
} else {
Column() {
Row() {
@@ -159,8 +160,8 @@ fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: A
item,
modifier = Modifier.drawReplyLevel(
item.replyLevel(),
MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
if (item.idHex == noteId) MaterialTheme.colors.primary.copy(alpha = 0.52f) else MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
MaterialTheme.colors.placeholderText,
if (item.idHex == noteId) MaterialTheme.colors.lessImportantLink else MaterialTheme.colors.placeholderText
),
parentBackgroundColor = background,
isBoostedNote = false,
@@ -284,7 +285,7 @@ fun NoteMaster(
Text(
timeAgo(note.createdAt(), context = context),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
maxLines = 1
)
@@ -296,7 +297,7 @@ fun NoteMaster(
imageVector = Icons.Default.MoreVert,
null,
modifier = Modifier.size(15.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
tint = MaterialTheme.colors.placeholderText
)
NoteDropDownMenu(baseNote, moreActionsExpanded, { moreActionsExpanded = false }, accountViewModel)
@@ -324,7 +325,7 @@ fun NoteMaster(
if (noteEvent is BadgeDefinitionEvent) {
BadgeDisplay(baseNote = note)
} else if (noteEvent is LongTextNoteEvent) {
Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp, bottom = 12.dp)) {
Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) {
Column {
noteEvent.image()?.let {
AsyncImage(
@@ -336,25 +337,23 @@ fun NoteMaster(
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(10.dp))
}
noteEvent.title()?.let {
Text(
text = it,
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp)
fontSize = 28.sp,
fontWeight = FontWeight.Light,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(10.dp))
}
noteEvent.summary()?.let {
Text(
text = it,
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp),
modifier = Modifier.fillMaxWidth(),
color = Color.Gray
)
}
@@ -402,47 +401,32 @@ fun NoteMaster(
accountViewModel,
nav
)
} else {
val eventContent = note.event?.content()
} else if (noteEvent is PollNoteEvent) {
val canPreview = note.author == account.userProfile() ||
(note.author?.let { account.userProfile().isFollowingCached(it) } ?: true) ||
!noteForReports.hasAnyReports()
if (eventContent != null) {
val hasSensitiveContent = remember(note.event) { note.event?.isSensitive() ?: false }
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
RenderPoll(
baseNote,
false,
canPreview,
backgroundColor,
accountViewModel,
nav
)
} else {
val canPreview = note.author == account.userProfile() ||
(note.author?.let { account.userProfile().isFollowingCached(it) } ?: true) ||
!noteForReports.hasAnyReports()
SensitivityWarning(
hasSensitiveContent = hasSensitiveContent,
accountViewModel = accountViewModel
) {
TranslatableRichTextViewer(
eventContent,
canPreview,
remember { Modifier.fillMaxWidth() },
tags,
backgroundColor,
accountViewModel,
nav
)
}
val hashtags = remember {
noteEvent.hashtags().toImmutableList()
}
DisplayUncitedHashtags(hashtags, eventContent, nav)
if (noteEvent is PollNoteEvent) {
PollNote(
note,
canPreview,
backgroundColor,
accountViewModel,
nav
)
}
}
RenderTextEvent(
baseNote,
false,
canPreview,
backgroundColor,
accountViewModel,
nav
)
}
}
}

View File

@@ -39,16 +39,25 @@ class AccountViewModel(val account: Account) : ViewModel() {
return account.userProfile()
}
fun reactTo(note: Note) {
account.reactTo(note)
fun reactTo(note: Note, reaction: String) {
account.reactTo(note, reaction)
}
fun hasReactedTo(baseNote: Note): Boolean {
return account.hasReacted(baseNote)
fun reactToOrDelete(note: Note, reaction: String) {
val currentReactions = account.reactionTo(note, reaction)
if (currentReactions.isNotEmpty()) {
account.delete(currentReactions)
} else {
account.reactTo(note, reaction)
}
}
fun deleteReactionTo(note: Note) {
account.delete(account.reactionTo(note))
fun hasReactedTo(baseNote: Note, reaction: String): Boolean {
return account.hasReacted(baseNote, reaction)
}
fun deleteReactionTo(note: Note, reaction: String) {
account.delete(account.reactionTo(note, reaction))
}
fun hasBoosted(baseNote: Note): Boolean {

View File

@@ -80,6 +80,7 @@ import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -281,7 +282,7 @@ fun DisplayReplyingToNote(
modifier = Modifier
.padding(end = 5.dp)
.size(30.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
tint = MaterialTheme.colors.placeholderText
)
}
}
@@ -318,7 +319,7 @@ fun EditFieldRow(
placeholder = {
Text(
text = stringResource(R.string.reply_here),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
@@ -334,7 +335,7 @@ fun EditFieldRow(
leadingIcon = {
UploadFromGallery(
isUploading = channelScreenModel.isUploadingImage,
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
tint = MaterialTheme.colors.placeholderText,
modifier = Modifier.padding(start = 5.dp)
) {
val fileServer = if (isPrivate) {
@@ -418,7 +419,7 @@ fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, nav:
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
"${channel.info.about}",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
color = MaterialTheme.colors.placeholderText,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
fontSize = 12.sp
@@ -489,7 +490,7 @@ private fun NoteCopyButton(
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
backgroundColor = MaterialTheme.colors.placeholderText
)
) {
Icon(

View File

@@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.ui.screen.ChatroomListFeedView
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -123,7 +124,7 @@ fun ChatroomListScreen(
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = null,
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
tint = MaterialTheme.colors.placeholderText
)
ChatroomTabMenu(

View File

@@ -36,6 +36,7 @@ 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.placeholderText
import kotlinx.coroutines.launch
@Composable
@@ -121,7 +122,7 @@ fun ConnectOrbotDialog(onClose: () -> Unit, onPost: () -> Unit, portNumber: Muta
placeholder = {
Text(
text = "9050",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
}
)

View File

@@ -2,10 +2,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.Checkbox
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Tab
import androidx.compose.material.TabRow
@@ -15,7 +17,11 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
@@ -23,6 +29,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.NostrHiddenAccountsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView
@@ -67,6 +74,34 @@ fun HiddenUsersScreen(
Column(modifier = Modifier.padding(start = 10.dp, end = 10.dp)) {
val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope()
var warnAboutReports by remember { mutableStateOf(accountViewModel.account.warnAboutPostsWithReports) }
var filterSpam by remember { mutableStateOf(accountViewModel.account.filterSpamFromStrangers) }
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = warnAboutReports,
onCheckedChange = {
warnAboutReports = it
accountViewModel.account.updateOptOutOptions(warnAboutReports, filterSpam)
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
}
)
Text(stringResource(R.string.warn_when_posts_have_reports_from_your_follows))
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = filterSpam,
onCheckedChange = {
filterSpam = it
accountViewModel.account.updateOptOutOptions(warnAboutReports, filterSpam)
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
}
)
Text(stringResource(R.string.filter_spam_from_strangers))
}
TabRow(
backgroundColor = MaterialTheme.colors.background,

View File

@@ -156,4 +156,9 @@ fun WatchAccountForHomeScreen(
}
@Immutable
class TabItem(val resource: Int, val viewModel: FeedViewModel, val routeForLastRead: String, val scrollStateKey: String)
class TabItem(
val resource: Int,
val viewModel: FeedViewModel,
val routeForLastRead: String,
val scrollStateKey: String
)

View File

@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.ui.navigation.Route
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -35,8 +36,11 @@ fun LoadRedirectScreen(eventId: String?, navController: NavController) {
val nav = remember(navController) {
{ route: String ->
navController.backQueue.removeLast()
navController.navigate(route)
navController.navigate(route) {
popUpTo(Route.Event.route) {
inclusive = true
}
}
}
}

View File

@@ -97,6 +97,7 @@ import com.vitorpamplona.amethyst.ui.screen.RelayFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -447,7 +448,7 @@ private fun ProfileHeader(
contentPadding = PaddingValues(0.dp)
) {
Icon(
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
tint = MaterialTheme.colors.placeholderText,
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(R.string.more_options)
)
@@ -610,7 +611,7 @@ private fun DrawAdditionalInfo(
CreateTextWithEmoji(
text = "@$it",
tags = tags,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
}
}
@@ -620,7 +621,7 @@ private fun DrawAdditionalInfo(
Text(
text = user.pubkeyDisplayHex(),
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
IconButton(
@@ -633,7 +634,7 @@ private fun DrawAdditionalInfo(
imageVector = Icons.Default.ContentCopy,
null,
modifier = Modifier.size(15.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
tint = MaterialTheme.colors.placeholderText
)
}
@@ -660,7 +661,7 @@ private fun DrawAdditionalInfo(
painter = painterResource(R.drawable.ic_qrcode),
null,
modifier = Modifier.size(15.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
tint = MaterialTheme.colors.placeholderText
)
}
}
@@ -673,7 +674,7 @@ private fun DrawAdditionalInfo(
if (!website.isNullOrEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
tint = MaterialTheme.colors.placeholderText,
imageVector = Icons.Default.Link,
contentDescription = stringResource(R.string.website),
modifier = Modifier.size(16.dp)
@@ -1224,7 +1225,7 @@ private fun MessageButton(user: User, nav: (String) -> Unit) {
shape = ButtonBorder,
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
backgroundColor = MaterialTheme.colors.placeholderText
)
) {
Icon(

View File

@@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableStateFlow
@@ -316,7 +317,7 @@ private fun SearchTextField(
placeholder = {
Text(
text = stringResource(R.string.npub_hex_username),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
trailingIcon = {

View File

@@ -87,6 +87,7 @@ import com.vitorpamplona.amethyst.ui.screen.LoadingFeed
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -301,7 +302,7 @@ private fun RenderVideoOrPicture(
remember {
Modifier
.padding(start = 10.dp, end = 10.dp)
.height(60.dp)
.height(65.dp)
.weight(1f)
},
verticalArrangement = Arrangement.Center
@@ -318,7 +319,7 @@ private fun RenderVideoOrPicture(
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(top = 5.dp)
modifier = Modifier.padding(top = 2.dp)
) {
RelayBadges(baseNote = note)
}
@@ -334,14 +335,14 @@ private fun VideoUserOptionAction(
var moreActionsExpanded by remember { mutableStateOf(false) }
IconButton(
modifier = remember { Modifier.size(24.dp) },
modifier = remember { Modifier.size(22.dp) },
onClick = { moreActionsExpanded = true }
) {
Icon(
imageVector = Icons.Default.MoreVert,
null,
modifier = remember { Modifier.size(20.dp) },
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
tint = MaterialTheme.colors.placeholderText
)
NoteDropDownMenu(
@@ -396,7 +397,7 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (St
BoostReaction(baseNote, accountViewModel, iconSize = 40.dp) {
wantsToQuote = baseNote
}*/
LikeReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp, heartSize = 35.dp)
LikeReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp, heartSize = 35.dp, 28.sp)
ZapReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp, animationSize = 35.dp)
ViewCountReaction(baseNote.idHex, grayTint = MaterialTheme.colors.onBackground, iconSize = 40.dp, barChartSize = 39.dp)
}
@@ -494,8 +495,8 @@ private fun ShowProgress(postViewModel: NewMediaModel) {
progress = postViewModel.uploadingPercentage.value,
modifier = Modifier
.size(55.dp)
.background(MaterialTheme.colors.background)
.clip(CircleShape),
.clip(CircleShape)
.background(MaterialTheme.colors.background),
strokeWidth = 5.dp
)
postViewModel.uploadingDescription.value?.let {

View File

@@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import java.util.*
@OptIn(ExperimentalComposeUiApi::class)
@@ -122,7 +123,7 @@ fun LoginPage(
placeholder = {
Text(
text = stringResource(R.string.nsec_npub_hex_private_key),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
color = MaterialTheme.colors.placeholderText
)
},
trailingIcon = {

View File

@@ -4,7 +4,6 @@ import android.app.Activity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.Colors
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Shapes
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
@@ -37,6 +36,30 @@ private val LightReplyItemBackground = LightColorPalette.onSurface.copy(alpha =
private val DarkSelectedNote = DarkNewItemBackground.compositeOver(DarkColorPalette.background)
private val LightSelectedNote = LightNewItemBackground.compositeOver(LightColorPalette.background)
private val DarkButtonBackground = DarkColorPalette.primary.copy(alpha = 0.32f).compositeOver(DarkColorPalette.background)
private val LightButtonBackground = LightColorPalette.primary.copy(alpha = 0.32f).compositeOver(LightColorPalette.background)
private val DarkLessImportantLink = DarkColorPalette.primary.copy(alpha = 0.52f)
private val LightLessImportantLink = LightColorPalette.primary.copy(alpha = 0.52f)
private val DarkMediumImportantLink = DarkColorPalette.primary.copy(alpha = 0.32f)
private val LightMediumImportantLink = LightColorPalette.primary.copy(alpha = 0.32f)
private val DarkVeryImportantLink = DarkColorPalette.primary.copy(alpha = 0.12f)
private val LightVeryImportantLink = LightColorPalette.primary.copy(alpha = 0.12f)
private val DarkPlaceholderText = DarkColorPalette.onSurface.copy(alpha = 0.32f)
private val LightPlaceholderText = LightColorPalette.onSurface.copy(alpha = 0.32f)
private val DarkSubtleBorder = DarkColorPalette.onSurface.copy(alpha = 0.12f)
private val LightSubtleBorder = LightColorPalette.onSurface.copy(alpha = 0.12f)
private val DarkImageVerifier = Nip05.copy(0.52f).compositeOver(DarkColorPalette.background)
private val LightImageVerifier = Nip05.copy(0.52f).compositeOver(LightColorPalette.background)
private val DarkOverPictureBackground = DarkColorPalette.background.copy(0.62f)
private val LightOverPictureBackground = LightColorPalette.background.copy(0.62f)
val Colors.newItemBackgroundColor: Color
get() = if (isLight) LightNewItemBackground else DarkNewItemBackground
@@ -46,6 +69,29 @@ val Colors.replyBackground: Color
val Colors.selectedNote: Color
get() = if (isLight) LightSelectedNote else DarkSelectedNote
val Colors.secondaryButtonBackground: Color
get() = if (isLight) LightButtonBackground else DarkButtonBackground
val Colors.lessImportantLink: Color
get() = if (isLight) LightLessImportantLink else DarkLessImportantLink
val Colors.mediumImportanceLink: Color
get() = if (isLight) LightMediumImportantLink else DarkMediumImportantLink
val Colors.veryImportantLink: Color
get() = if (isLight) LightVeryImportantLink else DarkVeryImportantLink
val Colors.placeholderText: Color
get() = if (isLight) LightPlaceholderText else DarkPlaceholderText
val Colors.subtleBorder: Color
get() = if (isLight) LightSubtleBorder else DarkSubtleBorder
val Colors.hashVerified: Color
get() = if (isLight) LightImageVerifier else DarkImageVerifier
val Colors.overPictureBackground: Color
get() = if (isLight) LightOverPictureBackground else DarkOverPictureBackground
@Composable
fun AmethystTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
val colors = if (darkTheme) {

View File

@@ -393,4 +393,6 @@
<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>
</resources>

View File

@@ -412,4 +412,9 @@
<string name="content_warning_see_warnings">Always show content warnings</string>
<string name="recommended_apps">Recommends: </string>
<string name="filter_spam_from_strangers">Filter spam from strangers</string>
<string name="warn_when_posts_have_reports_from_your_follows">Warn when posts have reports from your follows</string>
<string name="new_reaction_symbol">New Reaction Symbol</string>
<string name="no_reaction_type_setup_long_press_to_change">No reaction types selected. Long Press to change</string>
</resources>

View File

@@ -18,8 +18,7 @@ import java.util.regex.Pattern
data class ResultOrError(
val result: String?,
val sourceLang: String?,
val targetLang: String?,
val error: Exception?
val targetLang: String?
)
object LanguageTranslatorService {
@@ -78,7 +77,7 @@ object LanguageTranslatorService {
var fixedText = task.result.replace("# [", "#[") // fixes tags that always return with a space
results.add(decodeDictionary(fixedText, dict))
}
ResultOrError(results.joinToString("\n"), source, target, null)
ResultOrError(results.joinToString("\n"), source, target)
}
}
}

View File

@@ -36,6 +36,7 @@ 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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Locale
@@ -52,7 +53,7 @@ fun TranslatableRichTextViewer(
) {
Column {
var translatedTextState by remember {
mutableStateOf(ResultOrError(content, null, null, null))
mutableStateOf(ResultOrError(content, null, null))
}
var showOriginal by remember { mutableStateOf(false) }
@@ -117,7 +118,7 @@ private fun TranslationMessage(
.padding(top = 5.dp)
) {
val clickableTextStyle =
SpanStyle(color = MaterialTheme.colors.primary.copy(alpha = 0.52f))
SpanStyle(color = MaterialTheme.colors.lessImportantLink)
val annotatedTranslationString = buildAnnotatedString {
withStyle(clickableTextStyle) {

View File

@@ -1,9 +1,9 @@
buildscript {
ext {
fragment_version = "1.5.7"
fragment_version = "1.6.0"
lifecycle_version = '2.6.1'
compose_ui_version = '1.4.3'
nav_version = "2.5.3"
nav_version = "2.6.0"
room_version = "2.4.3"
accompanist_version = '0.30.0'
coil_version = '2.4.0'

View File

@@ -23,4 +23,5 @@ kotlin.code.style=official
android.nonTransitiveRClass=true
android.enableR8.fullMode=true
android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=false
android.nonFinalResIds=false
org.gradle.unsafe.configuration-cache=true