mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
Merge branch 'main' into fix/versionname-worktree-cwd
This commit is contained in:
@@ -19,13 +19,18 @@ Extract string resource keys from the default `values/strings.xml` that are abse
|
||||
|
||||
This repo syncs translations via Crowdin (branch `l10n_crowdin_translations`). Crowdin's default export behavior **omits any translation that exactly equals the source**, so a key that the translator deliberately kept as English (common for brand terms like `"Nowhere Drop"`, single-word loanwords like `"Apps"` / `"Feed"` / `"Issues"`, or version prefixes like `"v%1$s"`) will not appear in the locale's `strings.xml` even though the Crowdin UI shows it as 100% translated.
|
||||
|
||||
Consequences for this skill:
|
||||
What this means for this skill:
|
||||
|
||||
1. **A "missing" key on disk is not always actionable.** It may be Crowdin-stripped (translator already chose source-identical and Crowdin didn't export it) rather than genuinely new.
|
||||
2. **Don't add source-identical fallbacks locally.** Android's resource resolution falls back to `values/strings.xml` at runtime, so the user already sees the correct text. Local additions will be silently overwritten on Crowdin's next sync anyway.
|
||||
3. **The only actionable cases are keys Crowdin has never exported.** Whether the translator picked "use English" or simply hasn't translated the key yet, both states are owned by Crowdin and look identical on disk. The local repo cannot distinguish them.
|
||||
1. **The raw on-disk diff is the candidate set.** A key missing from a locale file is either genuinely untranslated *or* a source-identical entry Crowdin stripped. Both are reported; the human decides which to skip. The Crowdin web UI ("N untranslated") is the ground truth for what genuinely needs work.
|
||||
2. **Source-identical entries are a small, recognizable minority.** Brand terms (`Nowhere X`), single-word loanwords (`Apps` / `Feed` / `Issues`), and bare version/format strings (`v%1$s`) are the usual cases. Skip these by inspection rather than translating them to something identical.
|
||||
3. **Don't add source-identical fallbacks.** Android falls back to `values/strings.xml` at runtime, so a key intentionally kept as English already renders correctly, and Crowdin's next sync would strip a local duplicate anyway.
|
||||
|
||||
The Step 2.5 filter below uses the **most recent Crowdin export commit reachable from `HEAD`** (subject: `"New Crowdin translations by GitHub Action"`) as the cutoff: any key added to `values/strings.xml` after that commit is genuinely new (Crowdin hasn't exported it yet); anything older is Crowdin's responsibility regardless of why it's missing. The reachable-from-HEAD check survives the common workflow of deleting the `l10n_crowdin_translations` branch after merging.
|
||||
> **Historical note:** an earlier version of this skill tried to auto-filter the
|
||||
> candidate list with a git "sync-timestamp" heuristic (skip any key added before
|
||||
> the last `New Crowdin translations` commit). It was **dropped** because it
|
||||
> produced false negatives: a key added shortly before an export that translators
|
||||
> simply hadn't reached yet is genuinely missing, but the heuristic classified it
|
||||
> as "Crowdin already decided." Trust the raw diff + the Crowdin UI instead.
|
||||
|
||||
## Target Locales
|
||||
|
||||
@@ -72,74 +77,27 @@ comm -23 \
|
||||
| sed 's/.*name="\([^"]*\)".*/\1/' | sort)
|
||||
```
|
||||
|
||||
This gives two lists of missing key names — keep them separate; `<plurals>` translations need the per-locale CLDR category set (see Step 5 → "Plurals: handle with care"). Do NOT diff each locale separately for strings — assume the same keys are missing in all target locales (but DO repeat the `<plurals>` diff per locale if you suspect Crowdin asymmetric stripping).
|
||||
This gives two lists of missing key names — keep them separate; `<plurals>` translations need the per-locale CLDR category set (see Step 5 → "Plurals: handle with care").
|
||||
|
||||
> **Caveat:** Crowdin can asymmetrically strip keys across locales (each translator independently chose source-identical for different keys). If the cs-rCZ list looks suspiciously short, run the same diff for each target locale individually and union the results before Step 2.5.
|
||||
|
||||
### 2.5. Filter out keys Crowdin has already seen (sync-timestamp check)
|
||||
|
||||
A missing key is **only actionable if Crowdin has never exported it**. Once a key has been pushed to Crowdin and exported back, the translator may have chosen "use English" — Crowdin stores that choice in its own database and strips the entry from the exported `strings.xml`. From disk we cannot tell "translator picked English" from "Crowdin never saw the key": both look identical.
|
||||
|
||||
The reliable signal is **time**: compare when the key was added to `values/strings.xml` against the timestamp of the **most recent Crowdin export that has been merged into the current branch**. Crowdin's GitHub Action produces commits with the literal subject `New Crowdin translations by GitHub Action`; finding the latest such commit reachable from `HEAD` works even if the `l10n_crowdin_translations` branch has been deleted post-merge (a common cleanup workflow).
|
||||
|
||||
- Key added **before** that commit → Crowdin saw it on a prior export; translator made a decision; the absence on disk is a deliberate "use English" or "leave blank" choice. **Skip.**
|
||||
- Key added **after** → Crowdin has not exported it yet; genuinely new and actionable.
|
||||
Crowdin can asymmetrically strip keys across locales (each translator independently chose source-identical for different keys), so **cs-rCZ is not a reliable upper bound**. Diff **every** target locale and union the results — don't assume the cs-rCZ set covers the others. A quick per-locale count is a useful sanity check against the Crowdin UI's "N untranslated":
|
||||
|
||||
```bash
|
||||
# Latest Crowdin export reachable from HEAD (survives branch deletion).
|
||||
sync_ts=$(git log -1 --format=%ct --grep='^New Crowdin translations by GitHub Action$' 2>/dev/null)
|
||||
if [ -z "$sync_ts" ]; then
|
||||
echo "WARNING: no Crowdin export commit found in history; treating all missing as actionable" >&2
|
||||
sync_ts=0
|
||||
else
|
||||
echo "Crowdin sync cutoff: $(git log -1 --format='%ci %h' --grep='^New Crowdin translations by GitHub Action$')"
|
||||
fi
|
||||
|
||||
# For each locale, list only keys added after the Crowdin sync (truly new).
|
||||
# Run for BOTH <string> and <plurals>.
|
||||
for locale in cs-rCZ de-rDE sv-rSE; do
|
||||
echo "=== $locale: genuinely new (post-sync) <string> keys ==="
|
||||
comm -23 \
|
||||
for locale in cs-rCZ de-rDE sv-rSE pt-rBR; do
|
||||
ns=$(comm -23 \
|
||||
<(grep '<string name=' amethyst/src/main/res/values/strings.xml \
|
||||
| grep -v 'translatable="false"' \
|
||||
| sed 's/.*name="\([^"]*\)".*/\1/' | sort) \
|
||||
| grep -v 'translatable="false"' | sed 's/.*name="\([^"]*\)".*/\1/' | sort) \
|
||||
<(grep '<string name=' amethyst/src/main/res/values-$locale/strings.xml \
|
||||
| sed 's/.*name="\([^"]*\)".*/\1/' | sort) \
|
||||
| while IFS= read -r key; do
|
||||
added_ts=$(git log -1 --format=%ct -S "name=\"$key\"" -- amethyst/src/main/res/values/strings.xml)
|
||||
if [ -n "$added_ts" ] && [ "$added_ts" -gt "$sync_ts" ]; then
|
||||
echo "$key"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "=== $locale: genuinely new (post-sync) <plurals> keys ==="
|
||||
comm -23 \
|
||||
| sed 's/.*name="\([^"]*\)".*/\1/' | sort) | wc -l)
|
||||
np=$(comm -23 \
|
||||
<(grep '<plurals name=' amethyst/src/main/res/values/strings.xml \
|
||||
| sed 's/.*name="\([^"]*\)".*/\1/' | sort) \
|
||||
<(grep '<plurals name=' amethyst/src/main/res/values-$locale/strings.xml \
|
||||
| sed 's/.*name="\([^"]*\)".*/\1/' | sort) \
|
||||
| while IFS= read -r key; do
|
||||
added_ts=$(git log -1 --format=%ct -S "name=\"$key\"" -- amethyst/src/main/res/values/strings.xml)
|
||||
if [ -n "$added_ts" ] && [ "$added_ts" -gt "$sync_ts" ]; then
|
||||
echo "$key"
|
||||
fi
|
||||
done
|
||||
| sed 's/.*name="\([^"]*\)".*/\1/' | sort) | wc -l)
|
||||
echo "$locale: strings=$ns plurals=$np total=$((ns+np))"
|
||||
done
|
||||
```
|
||||
|
||||
**Why this beats using the `l10n_crowdin_translations` branch tip:**
|
||||
- The branch is often deleted after merge — the branch tip then doesn't exist or points to a stale ref.
|
||||
- The branch tip may include Crowdin commits that haven't been merged to main yet. Those changes aren't in our working tree, so they don't affect what's on disk for us. The "reachable from HEAD" cutoff matches what we can actually observe in `values-*/strings.xml`.
|
||||
|
||||
**Only the listed (post-sync) keys are actionable.** Anything older is either:
|
||||
- A deliberate "use English" choice in Crowdin (brand terms like `Nowhere X`, loanwords like `Apps` / `Feed` / `Issues`, version prefixes like `v%1$s`), or
|
||||
- A pending translation the translator hasn't filled in yet — still Crowdin's job, not ours.
|
||||
|
||||
In both cases, Android's resource resolution falls back to `values/strings.xml` at runtime, so there is no user-visible bug. Adding source-identical fallbacks locally is noise that the next sync will strip again.
|
||||
|
||||
Report the pre-sync skipped count as a one-liner ("N keys predate the last Crowdin sync, skipped — Crowdin owns them"). Do not list them or propose translations.
|
||||
|
||||
If no Crowdin export commit can be found in history (`sync_ts=0` fallback), warn the user and fall back to treating all missing keys as actionable — but flag that the workflow is degraded.
|
||||
The combined `strings + plurals` total should line up with the Crowdin web UI's untranslated count for that locale. If it does, the raw diff is your actionable set (minus any source-identical entries you skip by inspection — see Background).
|
||||
|
||||
### 3. Get English values for missing keys
|
||||
|
||||
@@ -301,10 +259,9 @@ When adding translated strings to locale files:
|
||||
|
||||
- **Forgetting `translatable="false"`** — these should never appear in locale files
|
||||
- **Diffing only `<string name=`** — `<plurals>` is a separate resource type; a source `<plurals>` missing from a locale will never show up in a `<string>` diff. Always run the diff twice (once per resource type) as shown in Step 2. The same goes for `<string-array>` if the project uses it.
|
||||
- **Treating every missing key as actionable** — Crowdin strips on export any translation the translator marked as "use English", and we cannot distinguish that from "never seen" by looking at disk. Use the Step 2.5 sync-timestamp filter: only keys added to `values/strings.xml` after the last `l10n_crowdin_translations` sync are genuinely new.
|
||||
- **Trying to detect "stripped" from git history alone** — the on-disk locale file only sees keys the translator typed a non-identical value for. The "translator opened the key and picked English from the start" case never touches disk, so a history-only check misses it. Use the sync-timestamp cutoff instead.
|
||||
- **Adding source-identical fallbacks locally** — they get overwritten on the next Crowdin sync. Android falls back to `values/strings.xml` at runtime anyway, so there is no user-visible bug to fix.
|
||||
- **Skipping per-locale diffs when only diffing cs-rCZ** — Crowdin can strip different keys in different locales (each translator's choice), so cs-rCZ is not a reliable upper bound. Diff each target locale, then apply the sync-timestamp filter.
|
||||
- **Trusting a git "sync-timestamp" heuristic to pre-filter the list** — this skill used to skip keys added before the last `New Crowdin translations` commit, on the theory that Crowdin had already "decided" them. It was dropped: a key added shortly before an export that translators hadn't reached yet is genuinely missing, so the heuristic silently dropped real work. Use the raw on-disk diff and reconcile against the Crowdin web UI's untranslated count instead.
|
||||
- **Adding source-identical fallbacks locally** — they get overwritten on the next Crowdin sync. Android falls back to `values/strings.xml` at runtime anyway, so a key intentionally kept as English already renders correctly. Skip these by inspection (brand terms, loanwords, `v%1$s`-style strings); don't translate them to an identical value.
|
||||
- **Skipping per-locale diffs when only diffing cs-rCZ** — Crowdin can strip different keys in different locales (each translator's choice), so cs-rCZ is not a reliable upper bound. Diff each target locale and union the results.
|
||||
- **Inserting strings in a specific position** — always append at the bottom; ordering is handled separately
|
||||
- **Hardcoding `"1"` in a `<plurals>` `quantity="one"` item** — always use the count placeholder; otherwise non-English `one` categories produce wrong text
|
||||
- **Copying English's `one`/`other` set into every locale** — each language must include all CLDR plural categories it uses (e.g. Czech needs `one`, `few`, `many`, `other`)
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuToken
|
||||
import com.vitorpamplona.amethyst.service.cashu.CashuParser
|
||||
import com.vitorpamplona.amethyst.service.cashu.CashuToken
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@@ -25,11 +25,11 @@ import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.core.content.edit
|
||||
import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.NwcWalletEntry
|
||||
import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.NwcWalletEntryNorm
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.TopFilter
|
||||
import com.vitorpamplona.amethyst.model.UiSettings
|
||||
import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcWalletEntry
|
||||
import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcWalletEntryNorm
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
|
||||
@@ -35,7 +35,13 @@ import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListD
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListState
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState
|
||||
import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.UserStatusAction
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.favoriteAlgoFeedsLists.FavoriteAlgoFeedsListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists.HashtagListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.muteList.MuteListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip56Reports.ReportAction
|
||||
import com.vitorpamplona.amethyst.commons.model.nip72Communities.CommunityListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.TrustProviderListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendResult
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendStage
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSender
|
||||
@@ -72,20 +78,16 @@ import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayLis
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.favoriteAlgoFeedsLists.FavoriteAlgoFeedsListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.favoriteAlgoFeedsLists.FavoriteAlgoFeedsListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.interestSets.InterestSetsState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkListsState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.FollowListsState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListsState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState
|
||||
@@ -97,7 +99,6 @@ import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayLis
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip62Vanish.VanishRequestsState
|
||||
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
|
||||
import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState
|
||||
import com.vitorpamplona.amethyst.model.nipA3PaymentTargets.NipA3PaymentTargetsState
|
||||
@@ -111,7 +112,6 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.FeedDecryptionCaches
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.FeedTopNavFilterState
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState
|
||||
import com.vitorpamplona.amethyst.model.trustedAssertions.TrustProviderListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.trustedAssertions.TrustProviderListState
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
||||
@@ -192,6 +192,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
|
||||
import com.vitorpamplona.quartz.nip32Labeling.LabelEvent
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEventCache
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
@@ -741,6 +742,48 @@ class Account(
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* NIP-32: tags [note] with [hashtag] by publishing a kind 1985 label event using the
|
||||
* `#t` tag-association namespace. Fire-and-forget; signs and broadcasts immediately.
|
||||
*/
|
||||
suspend fun labelHashtag(
|
||||
note: Note,
|
||||
hashtag: String,
|
||||
) {
|
||||
createLabelHashtagEvent(note, hashtag)?.let { (event, relays) ->
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
client.publish(event, relays)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and signs a NIP-32 hashtag label event for [note] without sending it.
|
||||
* Returns the signed event and target relays for tracked broadcasting, or null if
|
||||
* the account can't write or the note has no underlying event.
|
||||
*/
|
||||
suspend fun createLabelHashtagEvent(
|
||||
note: Note,
|
||||
hashtag: String,
|
||||
): Pair<Event, Set<NormalizedRelayUrl>>? {
|
||||
if (!signer.isWriteable()) return null
|
||||
|
||||
val eventHint = note.toEventHint<Event>() ?: return null
|
||||
|
||||
val template = LabelEvent.buildHashtagLabel(eventHint, hashtag)
|
||||
|
||||
val event = signer.sign(template)
|
||||
val relays = computeRelayListToBroadcast(event)
|
||||
|
||||
return event to relays
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes a label event into local cache. Called when tracked broadcasting succeeds.
|
||||
*/
|
||||
fun consumeLabelEvent(event: Event) {
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
|
||||
suspend fun createZapRequestFor(
|
||||
event: Event,
|
||||
pollOption: Int?,
|
||||
|
||||
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.model
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatRepository
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListRepository
|
||||
import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcWalletEntryNorm
|
||||
import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.NwcWalletEntryNorm
|
||||
import com.vitorpamplona.amethyst.model.nip60Cashu.CashuPreferences
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
@@ -385,21 +385,6 @@ class AccountSettings(
|
||||
return false
|
||||
}
|
||||
|
||||
fun moveNwcWallet(
|
||||
fromIndex: Int,
|
||||
toIndex: Int,
|
||||
): Boolean {
|
||||
val wallets = nwcWallets.value.toMutableList()
|
||||
if (fromIndex in wallets.indices && toIndex in wallets.indices && fromIndex != toIndex) {
|
||||
val item = wallets.removeAt(fromIndex)
|
||||
wallets.add(toIndex, item)
|
||||
nwcWallets.tryEmit(wallets)
|
||||
saveAccountSettings()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URINorm?): Boolean {
|
||||
if (newServer == null) {
|
||||
if (nwcWallets.value.isNotEmpty()) {
|
||||
|
||||
@@ -143,6 +143,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.nip32Labeling.LabelEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.grasp.UserGraspListEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
|
||||
@@ -1525,6 +1526,43 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
return false
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: LabelEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
): Boolean {
|
||||
val note = getOrCreateNote(event.id)
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event != null) return true
|
||||
|
||||
if (wasVerified || justVerify(event)) {
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
val repliesTo = computeReplyTo(event)
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
// Attach NIP-32 hashtag labels (namespace #t) to the labeled events so the
|
||||
// hashtag feed can surface posts tagged by a follow and attribute the labeler.
|
||||
val hashtags = event.hashtagAssociations()
|
||||
if (hashtags.isNotEmpty()) {
|
||||
event.labeledEvents().mapNotNull { checkGetOrCreateNote(it) }.forEach { target ->
|
||||
hashtags.forEach { hashtag -> target.addLabel(hashtag, note) }
|
||||
|
||||
// If the labeled post is already in cache, re-notify feed observers so the
|
||||
// hashtag feed can pick it up now that a (possibly followed) user labeled it.
|
||||
if (target.event != null) refreshNewNoteObservers(target)
|
||||
}
|
||||
}
|
||||
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: ReportEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
@@ -3730,6 +3768,10 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
consume(event, relay, wasVerified)
|
||||
}
|
||||
|
||||
is LabelEvent -> {
|
||||
consume(event, relay, wasVerified)
|
||||
}
|
||||
|
||||
is ContactCardEvent -> {
|
||||
consume(event, relay, wasVerified)
|
||||
}
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip30CustomEmojis
|
||||
import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent
|
||||
import com.vitorpamplona.amethyst.commons.model.eventIdSet
|
||||
import com.vitorpamplona.amethyst.commons.model.events
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.OwnedEmojiPack
|
||||
import com.vitorpamplona.amethyst.commons.model.updateFlow
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
|
||||
+1
-1
@@ -20,10 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListDecryptionCache
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||
|
||||
+1
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.favoriteAlgoFeedsLists
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.favoriteAlgoFeedsLists.FavoriteAlgoFeedsListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
|
||||
+1
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists.HashtagListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip51Lists.interestSets
|
||||
import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent
|
||||
import com.vitorpamplona.amethyst.commons.model.eventIdSet
|
||||
import com.vitorpamplona.amethyst.commons.model.events
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestSets.InterestSet
|
||||
import com.vitorpamplona.amethyst.commons.model.updateFlow
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists
|
||||
import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent
|
||||
import com.vitorpamplona.amethyst.commons.model.eventIdSet
|
||||
import com.vitorpamplona.amethyst.commons.model.events
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
|
||||
import com.vitorpamplona.amethyst.commons.model.updateFlow
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
|
||||
+1
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.muteList
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.muteList.MuteListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip51Lists.peopleList
|
||||
import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent
|
||||
import com.vitorpamplona.amethyst.commons.model.eventIdSet
|
||||
import com.vitorpamplona.amethyst.commons.model.events
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.updateFlow
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
|
||||
+16
@@ -203,6 +203,22 @@ class CashuWalletState(
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(scope, SharingStarted.Eagerly, 0L)
|
||||
|
||||
/**
|
||||
* Spendable cashu balance per mint URL — the reactive twin of
|
||||
* [peekMintBalances], for UI that needs to re-render as proofs arrive
|
||||
* (the wallet screen's mint list). Derived from [_tokenEntries] exactly
|
||||
* like [balanceSats]. May overstate by NUT-07 stale proofs; the actual
|
||||
* spend scrubs before melting.
|
||||
*/
|
||||
val mintBalances: StateFlow<Map<String, Long>> =
|
||||
_tokenEntries
|
||||
.map { entries ->
|
||||
entries
|
||||
.groupBy { it.content.mint }
|
||||
.mapValues { (_, byMint) -> byMint.sumOf { it.content.totalAmount() } }
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
|
||||
|
||||
private val _history = MutableStateFlow<List<CashuSpendingHistoryEvent>>(emptyList())
|
||||
val history: StateFlow<List<CashuSpendingHistoryEvent>> = _history.asStateFlow()
|
||||
|
||||
|
||||
+1
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip72Communities
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.nip72Communities.CommunityListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
|
||||
+4
-4
@@ -20,11 +20,11 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.topNavFeeds
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists.HashtagListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.muteList.MuteListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip72Communities.CommunityListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListDecryptionCache
|
||||
|
||||
class FeedDecryptionCaches(
|
||||
val peopleListCache: PeopleListDecryptionCache,
|
||||
|
||||
+2
-1
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.trustedAssertions
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.TrustProviderListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
@@ -41,7 +42,7 @@ import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import com.vitorpamplona.amethyst.commons.model.trustedAssertions.TrustProviderListState as ITrustProviderListState
|
||||
import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.TrustProviderListState as ITrustProviderListState
|
||||
|
||||
class TrustProviderListState(
|
||||
val signer: NostrSigner,
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun countToHumanReadableBytes(counter: Int) =
|
||||
when {
|
||||
counter >= 1000000000 -> "${(counter / 1000000000f).roundToInt()} GB"
|
||||
counter >= 1000000 -> "${(counter / 1000000f).roundToInt()} MB"
|
||||
counter >= 1000 -> "${(counter / 1000f).roundToInt()} KB"
|
||||
else -> "$counter"
|
||||
}
|
||||
|
||||
fun countToHumanReadableBytes(counter: Long) =
|
||||
when {
|
||||
counter >= 1000000000 -> "${(counter / 1000000000f).roundToInt()} GB"
|
||||
counter >= 1000000 -> "${(counter / 1000000f).roundToInt()} MB"
|
||||
counter >= 1000 -> "${(counter / 1000f).roundToInt()} KB"
|
||||
else -> "$counter"
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun countToHumanReadable(
|
||||
counter: Int,
|
||||
str: String,
|
||||
) = when {
|
||||
counter >= 1000000000 -> "${(counter / 1000000000f).roundToInt()}G $str"
|
||||
counter >= 1000000 -> "${(counter / 1000000f).roundToInt()}M $str"
|
||||
counter >= 1000 -> "${(counter / 1000f).roundToInt()}K $str"
|
||||
else -> "$counter $str"
|
||||
}
|
||||
|
||||
fun countToHumanReadable(
|
||||
counter: Long,
|
||||
str: String,
|
||||
) = when {
|
||||
counter >= 1000000000 -> "${(counter / 1000000000f).roundToInt()}G $str"
|
||||
counter >= 1000000 -> "${(counter / 1000000f).roundToInt()}M $str"
|
||||
counter >= 1000 -> "${(counter / 1000f).roundToInt()}K $str"
|
||||
else -> "$counter $str"
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
|
||||
fun String.isUTF16Char(pos: Int): Boolean = Character.charCount(this.codePointAt(pos)) == 2
|
||||
|
||||
fun String.firstFullCharOld(): String =
|
||||
when (this.length) {
|
||||
0, 1 -> {
|
||||
this
|
||||
}
|
||||
|
||||
2, 3 -> {
|
||||
if (isUTF16Char(0)) this.take(2) else this.take(1)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val first = isUTF16Char(0)
|
||||
val second = isUTF16Char(2)
|
||||
if (first && second) {
|
||||
this.take(4)
|
||||
} else if (first) {
|
||||
this.take(2)
|
||||
} else {
|
||||
this.take(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.firstFullChar(): String {
|
||||
var isInJoin = false
|
||||
var hasHadSecondChance = false
|
||||
var start = 0
|
||||
var previousCharLength = 0
|
||||
var next: Int
|
||||
var codePoint: Int
|
||||
|
||||
var i = 0
|
||||
|
||||
while (i < this.length) {
|
||||
codePoint = codePointAt(i)
|
||||
|
||||
// Skips if it starts with the join char 0x200D
|
||||
if (codePoint == 0x200D && previousCharLength == 0) {
|
||||
next = offsetByCodePoints(i, 1)
|
||||
start = next
|
||||
} else {
|
||||
// If join, searches for the next char
|
||||
if (codePoint == 0xFE0F) {
|
||||
} else if (codePoint == 0x200D) {
|
||||
isInJoin = true
|
||||
} else {
|
||||
// stops when two chars are not joined together
|
||||
if (previousCharLength > 0 && !isInJoin) {
|
||||
if (Character.charCount(codePoint) == 1 || hasHadSecondChance) {
|
||||
break
|
||||
} else {
|
||||
hasHadSecondChance = true
|
||||
}
|
||||
} else {
|
||||
hasHadSecondChance = false
|
||||
}
|
||||
|
||||
isInJoin = false
|
||||
}
|
||||
|
||||
// next char to evaluate
|
||||
next = offsetByCodePoints(i, 1)
|
||||
previousCharLength += (next - i)
|
||||
}
|
||||
|
||||
i = next
|
||||
}
|
||||
|
||||
// if ends in join, then seachers backwards until a char is found.
|
||||
if (isInJoin) {
|
||||
i = previousCharLength - 1
|
||||
while (i > 0) {
|
||||
if (this[i].code == 0x200D) {
|
||||
previousCharLength -= 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
i -= 1
|
||||
}
|
||||
}
|
||||
|
||||
return substring(start, start + previousCharLength)
|
||||
}
|
||||
|
||||
fun String.firstFullCharOrEmoji(tags: ImmutableListOfLists<String>): String {
|
||||
if (length <= 2) {
|
||||
return firstFullChar()
|
||||
}
|
||||
|
||||
if (this[0] == ':') {
|
||||
// makes sure an emoji exists
|
||||
val emojiParts = this.split(":", limit = 3)
|
||||
if (emojiParts.size >= 2) {
|
||||
val emojiName = emojiParts[1]
|
||||
val emojiUrl = tags.lists.firstOrNull { it.size > 1 && it[1] == emojiName }?.getOrNull(2)
|
||||
if (emojiUrl != null) {
|
||||
return ":$emojiName:$emojiUrl"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (EmojiCoder.isCoded(this)) {
|
||||
return EmojiCoder.cropToFirstMessage(this)
|
||||
}
|
||||
|
||||
return firstFullChar()
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.service.cashu
|
||||
|
||||
import android.util.LruCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuToken
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.cashu
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuToken
|
||||
import com.vitorpamplona.amethyst.service.cashu.v3.V3Parser
|
||||
import com.vitorpamplona.amethyst.service.cashu.v4.V4Parser
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ import android.content.Context
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.cashu.CashuToken
|
||||
import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuToken
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.asTextOrNull
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.cashu.melt
|
||||
|
||||
import com.vitorpamplona.amethyst.service.cashu.CashuToken
|
||||
import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuToken
|
||||
|
||||
class MeltResult(
|
||||
val token: CashuToken,
|
||||
|
||||
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.cashu.v3
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.service.cashu.CashuToken
|
||||
import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuToken
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.cashu.v3
|
||||
|
||||
import com.vitorpamplona.amethyst.service.cashu.Proof
|
||||
import com.vitorpamplona.amethyst.commons.model.nip60Cashu.Proof
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.cashu.v4
|
||||
|
||||
import com.vitorpamplona.amethyst.service.cashu.CashuToken
|
||||
import com.vitorpamplona.amethyst.service.cashu.Proof
|
||||
import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuToken
|
||||
import com.vitorpamplona.amethyst.commons.model.nip60Cashu.Proof
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@@ -55,8 +55,8 @@ import androidx.core.net.toUri
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.Cashu
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
|
||||
import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuToken
|
||||
import com.vitorpamplona.amethyst.service.cashu.CachedCashuParser
|
||||
import com.vitorpamplona.amethyst.service.cashu.CashuToken
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.util.setText
|
||||
import com.vitorpamplona.amethyst.ui.note.CopyIcon
|
||||
|
||||
@@ -201,6 +201,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.CashuWalletScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.CashuWalletSettingsScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.OnchainTransactionsScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.ReloadMintScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.TopUpMintScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletDetailScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletReceiveScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletScreen
|
||||
@@ -348,6 +349,8 @@ fun BuildNavigation(
|
||||
|
||||
composableFromBottomArgs<Route.ReloadMint> { ReloadMintScreen(it.requestId, accountViewModel, nav) }
|
||||
|
||||
composableFromBottomArgs<Route.TopUpMint> { TopUpMintScreen(it.mintUrl, accountViewModel, nav) }
|
||||
|
||||
composableFromBottomArgs<Route.EditProfile> { NewUserMetadataScreen(nav, accountViewModel) }
|
||||
composable<Route.Search> { SearchScreen(accountViewModel, nav) }
|
||||
|
||||
|
||||
@@ -650,6 +650,11 @@ sealed class Route {
|
||||
data class ReloadMint(
|
||||
val requestId: String,
|
||||
) : Route()
|
||||
|
||||
@Serializable
|
||||
data class TopUpMint(
|
||||
val mintUrl: String,
|
||||
) : Route()
|
||||
}
|
||||
|
||||
inline fun <reified T : Route> isBaseRoute(navController: NavHostController): Boolean = navController.currentBackStackEntry?.destination?.hasRoute<T>() == true
|
||||
|
||||
+1
-1
@@ -73,9 +73,9 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||
import com.vitorpamplona.amethyst.commons.util.firstFullChar
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.service.firstFullChar
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMapNotNull
|
||||
import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.elements
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
/**
|
||||
* NIP-32: lets the user tag any post with a hashtag by publishing a kind 1985 label
|
||||
* event (under the `#t` tag-association namespace). The label is public so that the
|
||||
* author's and the labeler's followers can discover the post in the hashtag feed.
|
||||
*/
|
||||
@Composable
|
||||
fun AddHashtagLabelDialog(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var hashtag by remember { mutableStateOf("") }
|
||||
|
||||
// Strip the leading '#', drop whitespace and lowercase so the stored label matches the
|
||||
// hashtag-feed convention. A blank result disables the confirm button.
|
||||
val sanitized =
|
||||
hashtag
|
||||
.trim()
|
||||
.removePrefix("#")
|
||||
.filterNot { it.isWhitespace() }
|
||||
.lowercase()
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringRes(R.string.add_hashtag_label_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(stringRes(R.string.add_hashtag_label_explainer))
|
||||
OutlinedTextField(
|
||||
value = hashtag,
|
||||
onValueChange = { hashtag = it },
|
||||
singleLine = true,
|
||||
label = { Text(stringRes(R.string.add_hashtag_label_field)) },
|
||||
prefix = { Text("#") },
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = sanitized.isNotEmpty(),
|
||||
onClick = {
|
||||
accountViewModel.labelWithHashtag(note, sanitized)
|
||||
onDismiss()
|
||||
},
|
||||
) {
|
||||
Text(stringRes(R.string.add_hashtag_label_confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringRes(R.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -115,6 +115,7 @@ fun NoteDropDownMenu(
|
||||
nav: INav,
|
||||
) {
|
||||
var reportDialogShowing by remember { mutableStateOf(false) }
|
||||
var addLabelDialogShowing by remember { mutableStateOf(false) }
|
||||
|
||||
val state by observeBookmarksFollowsAndAccount(note, accountViewModel).collectAsStateWithLifecycle(
|
||||
DropDownParams(
|
||||
@@ -286,6 +287,9 @@ fun NoteDropDownMenu(
|
||||
}
|
||||
}
|
||||
}
|
||||
M3ActionRow(icon = MaterialSymbols.Tag, text = stringRes(R.string.add_hashtag_label)) {
|
||||
addLabelDialogShowing = true
|
||||
}
|
||||
// Pick exactly one curation flow per kind: music tracks go to playlists, emoji
|
||||
// packs go to the emoji list, everything else gets the standard bookmark rows.
|
||||
// Showing both at once is noisy and makes "bookmark" feel like the catch-all when
|
||||
@@ -387,6 +391,17 @@ fun NoteDropDownMenu(
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
if (addLabelDialogShowing) {
|
||||
AddHashtagLabelDialog(
|
||||
note = note,
|
||||
accountViewModel = accountViewModel,
|
||||
onDismiss = {
|
||||
addLabelDialogShowing = false
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -49,9 +49,9 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.util.countToHumanReadableBytes
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.countToHumanReadableBytes
|
||||
import com.vitorpamplona.amethyst.ui.components.ShowMoreButton
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
|
||||
@@ -45,9 +45,9 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.util.countToHumanReadableBytes
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.countToHumanReadableBytes
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
|
||||
@@ -51,10 +51,22 @@ fun RefresheableFeedView(
|
||||
scrollStateKey: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
onLoaded: (@Composable (FeedState.Loaded, LazyListState) -> Unit)? = null,
|
||||
) {
|
||||
RefresheableBox(viewModel, enablePullRefresh) {
|
||||
SaveableFeedState(viewModel.feedState, scrollStateKey) { listState ->
|
||||
RenderFeedState(viewModel, accountViewModel, listState, nav, routeForLastRead)
|
||||
if (onLoaded != null) {
|
||||
RenderFeedState(
|
||||
viewModel,
|
||||
accountViewModel,
|
||||
listState,
|
||||
nav,
|
||||
routeForLastRead,
|
||||
onLoaded = { onLoaded(it, listState) },
|
||||
)
|
||||
} else {
|
||||
RenderFeedState(viewModel, accountViewModel, listState, nav, routeForLastRead)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ import android.content.Context
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestSets.InterestSet
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.TopFilter
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.interestSets.InterestSet
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
+11
-1
@@ -41,6 +41,7 @@ import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers
|
||||
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuToken
|
||||
import com.vitorpamplona.amethyst.commons.model.observables.CreatedAtComparator
|
||||
import com.vitorpamplona.amethyst.commons.nipACWebRtcCalls.CallManager
|
||||
import com.vitorpamplona.amethyst.commons.tor.TorType
|
||||
@@ -63,7 +64,6 @@ import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilde
|
||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.service.broadcast.BroadcastTracker
|
||||
import com.vitorpamplona.amethyst.service.cashu.CashuToken
|
||||
import com.vitorpamplona.amethyst.service.cashu.melt.MeltProcessor
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
@@ -1068,6 +1068,16 @@ class AccountViewModel(
|
||||
direct = { account.removeBookmark(note, false) },
|
||||
)
|
||||
|
||||
/** NIP-32: tags [note] with [hashtag] by publishing a kind 1985 label event. */
|
||||
fun labelWithHashtag(
|
||||
note: Note,
|
||||
hashtag: String,
|
||||
) = launchTrackedOrDirect(
|
||||
createTracked = { account.createLabelHashtagEvent(note, hashtag) },
|
||||
consumeTracked = account::consumeLabelEvent,
|
||||
direct = { account.labelHashtag(note, hashtag) },
|
||||
)
|
||||
|
||||
fun removeDeletedBookmarks(
|
||||
deletedEventIds: Set<String>,
|
||||
deletedAddresses: Set<Address>,
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
|
||||
+1
-1
@@ -42,10 +42,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.OldBookmarkListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.PinListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
|
||||
+1
-1
@@ -35,10 +35,10 @@ import androidx.compose.ui.Modifier
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.OldBookmarkListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.PinListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.fabBottomBarPadding
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
|
||||
+1
-1
@@ -31,8 +31,8 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
|
||||
import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException
|
||||
import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
|
||||
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||
|
||||
+1
-1
@@ -36,8 +36,8 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.FabBottomBarPadded
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
|
||||
+1
-1
@@ -36,8 +36,8 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.FabBottomBarPadded
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.model.nip30CustomEmojis.OwnedEmojiPack
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.OwnedEmojiPack
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.nip30CustomEmojis.OwnedEmojiPack
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.OwnedEmojiPack
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
|
||||
+1
-1
@@ -31,8 +31,8 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.OwnedEmojiPack
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.nip30CustomEmojis.OwnedEmojiPack
|
||||
import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException
|
||||
import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
|
||||
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash
|
||||
|
||||
/**
|
||||
* Hashtag feed renderer that adds an attribution banner above any post that a followed user
|
||||
* surfaced through a NIP-32 hashtag label (rather than the author tagging it directly).
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun HashtagFeedLoaded(
|
||||
tag: String,
|
||||
loaded: FeedState.Loaded,
|
||||
listState: LazyListState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val items by loaded.feed.collectAsStateWithLifecycle()
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = rememberFeedContentPadding(FeedPadding),
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(
|
||||
items.list,
|
||||
key = { _, item -> item.idHex },
|
||||
contentType = { _, item -> item.event?.kind ?: -1 },
|
||||
) { _, item ->
|
||||
Column(Modifier.fillMaxWidth().animateItem()) {
|
||||
HashtagLabelAttribution(item, tag, accountViewModel)
|
||||
|
||||
NoteCompose(
|
||||
item,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
routeForLastRead = null,
|
||||
isBoostedNote = false,
|
||||
isHiddenFeed = items.showHidden,
|
||||
quotesLeft = 3,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows "🏷 #tag added by <user>" when a followed user labeled this post with the hashtag
|
||||
* and the author didn't include it themselves. Renders nothing otherwise.
|
||||
*/
|
||||
@Composable
|
||||
fun HashtagLabelAttribution(
|
||||
note: Note,
|
||||
tag: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val labelState by note
|
||||
.flow()
|
||||
.labels.stateFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
// The author already used the hashtag — nothing to attribute.
|
||||
if (note.event?.isTaggedHash(tag) == true) return
|
||||
|
||||
val follows = accountViewModel.account.followingKeySet()
|
||||
val labeler =
|
||||
labelState.note.labels[tag.lowercase()]
|
||||
?.firstOrNull { it.author?.pubkeyHex in follows }
|
||||
?.author ?: return
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().then(HalfStartPadding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Tag,
|
||||
contentDescription = null,
|
||||
modifier = Size16Modifier,
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
Spacer(StdHorzSpacer)
|
||||
Text(
|
||||
"#$tag",
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
)
|
||||
Spacer(StdHorzSpacer)
|
||||
Text(
|
||||
stringRes(R.string.hashtag_label_added_by),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
)
|
||||
Spacer(StdHorzSpacer)
|
||||
UsernameDisplay(
|
||||
labeler,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textColor = MaterialTheme.colorScheme.placeholderText,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
+3
@@ -113,6 +113,9 @@ fun HashtagScreen(
|
||||
null,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onLoaded = { state, listState ->
|
||||
HashtagFeedLoaded(tag.hashtag, state, listState, accountViewModel, nav)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+28
-3
@@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent
|
||||
import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
@@ -53,9 +54,10 @@ class HashtagFeedFilter(
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + tag
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val follows = account.followingKeySet()
|
||||
val notes =
|
||||
cache.notes.filterIntoSet { _, it ->
|
||||
acceptableEvent(it, tag)
|
||||
acceptableEvent(it, tag, follows)
|
||||
}
|
||||
|
||||
return sort(notes)
|
||||
@@ -63,16 +65,39 @@ class HashtagFeedFilter(
|
||||
|
||||
override fun applyFilter(newItems: Set<Note>): Set<Note> = innerApplyFilter(newItems)
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> = collection.filterTo(HashSet()) { acceptableEvent(it, tag) }
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val follows = account.followingKeySet()
|
||||
return collection.filterTo(HashSet()) { acceptableEvent(it, tag, follows) }
|
||||
}
|
||||
|
||||
fun acceptableEvent(
|
||||
it: Note,
|
||||
hashTag: String,
|
||||
follows: Set<HexKey>,
|
||||
): Boolean =
|
||||
(acceptableViaHashtag(it.event, hashTag) || acceptableViaScope(it.event, hashTag)) &&
|
||||
(
|
||||
acceptableViaHashtag(it.event, hashTag) ||
|
||||
acceptableViaScope(it.event, hashTag) ||
|
||||
acceptableViaFollowLabel(it, hashTag, follows)
|
||||
) &&
|
||||
!it.isHiddenFor(account.hiddenUsers.flow.value) &&
|
||||
account.isAcceptable(it)
|
||||
|
||||
/**
|
||||
* NIP-32: accept a post that a followed user has tagged with this hashtag via a kind
|
||||
* 1985 label event (namespace `#t`), even if the post's author never used the hashtag.
|
||||
* Requires the target to actually have an event so it can render.
|
||||
*/
|
||||
fun acceptableViaFollowLabel(
|
||||
note: Note,
|
||||
hashTag: String,
|
||||
follows: Set<HexKey>,
|
||||
): Boolean {
|
||||
if (note.event == null) return false
|
||||
val labelers = note.labels[hashTag.lowercase()] ?: return false
|
||||
return labelers.any { it.author?.pubkeyHex in follows }
|
||||
}
|
||||
|
||||
fun acceptableViaHashtag(
|
||||
event: Event?,
|
||||
hashTag: String,
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindEvent
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts
|
||||
import com.vitorpamplona.quartz.nip32Labeling.LabelEvent
|
||||
import com.vitorpamplona.quartz.utils.mapOfSet
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Builds the relay filters that power the NIP-32 side of the hashtag feed:
|
||||
*
|
||||
* 1. A subscription for kind 1985 label events that tag posts with this hashtag (the `l`
|
||||
* tag under the `#t` namespace), routed through the NIP-65 outbox model: each follow's
|
||||
* labels are requested from THAT follow's outbox relays, with the relay's filter
|
||||
* restricted to the follows that actually write there. This is what guarantees we pick
|
||||
* up a follow's labels even if they never reach the hashtag's own relays.
|
||||
* 2. Requests for the underlying posts a *followed* user has labeled but that aren't in
|
||||
* cache yet, so they can actually render in the feed. This rotates as new label
|
||||
* events arrive (the sub-assembler re-runs after each EOSE).
|
||||
*/
|
||||
fun filterHashtagLabels(
|
||||
account: Account,
|
||||
hashtag: String,
|
||||
relays: Set<NormalizedRelayUrl>,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> {
|
||||
val labelValues = hashtagAlts(hashtag).sorted()
|
||||
|
||||
// Outbox routing: relay -> the follows who publish to it.
|
||||
val followsPerRelay = account.followsPerRelay.value
|
||||
val labelFilters =
|
||||
followsPerRelay.mapNotNull { (relay, authors) ->
|
||||
if (authors.isEmpty()) return@mapNotNull null
|
||||
val authorList = authors.sorted()
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = listOf(LabelEvent.KIND),
|
||||
authors = authorList,
|
||||
tags = mapOf("l" to labelValues),
|
||||
limit = min(authorList.size * 5, 500),
|
||||
since = since?.get(relay)?.time,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val follows = account.followingKeySet()
|
||||
|
||||
// Collect the posts that a followed user labeled with this hashtag but that we don't
|
||||
// have the full event for yet, and ask their likely relays for them.
|
||||
val labelNotes =
|
||||
LocalCache.notes.filterIntoSet { _, note ->
|
||||
val noteEvent = note.event
|
||||
noteEvent is LabelEvent &&
|
||||
noteEvent.pubKey in follows &&
|
||||
hashtag in noteEvent.hashtagAssociations()
|
||||
}
|
||||
|
||||
val missingTargets =
|
||||
mapOfSet {
|
||||
labelNotes.forEach { labelNote ->
|
||||
(labelNote.event as LabelEvent).labeledEvents().forEach { targetId ->
|
||||
val target = LocalCache.getNoteIfExists(targetId)
|
||||
if (target?.event == null) {
|
||||
val targetNote = LocalCache.getOrCreateNote(targetId)
|
||||
potentialRelaysToFindEvent(targetNote).ifEmpty { relays }.forEach { relayUrl ->
|
||||
add(relayUrl, targetId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return labelFilters + filterMissingEvents(missingTargets)
|
||||
}
|
||||
+3
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
@@ -28,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
class HashtagQueryState(
|
||||
val hashtag: String,
|
||||
val relays: Set<NormalizedRelayUrl>,
|
||||
val account: Account,
|
||||
) {
|
||||
val lowercaseHashtag = hashtag.lowercase()
|
||||
}
|
||||
@@ -38,6 +40,7 @@ class HashtagFilterAssembler(
|
||||
val group =
|
||||
listOf(
|
||||
HashtagFeedFilterSubAssembler(client, ::allKeys),
|
||||
HashtagLabelSubAssembler(client, ::allKeys),
|
||||
)
|
||||
|
||||
override fun invalidateKeys() = invalidateFilters()
|
||||
|
||||
+5
-1
@@ -35,7 +35,11 @@ fun HashtagFilterAssemblerSubscription(
|
||||
// even if they are tracking the same tag.
|
||||
val state =
|
||||
remember(tag) {
|
||||
HashtagQueryState(tag.hashtag, accountViewModel.account.followOutboxesOrProxy.flow.value)
|
||||
HashtagQueryState(
|
||||
tag.hashtag,
|
||||
accountViewModel.account.followOutboxesOrProxy.flow.value,
|
||||
accountViewModel.account,
|
||||
)
|
||||
}
|
||||
|
||||
LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().hashtags)
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
|
||||
/**
|
||||
* NIP-32: streams kind 1985 label events that tag posts with the screen's hashtag and
|
||||
* pulls in the posts a followed user labeled. Rotates after each EOSE (`invalidateAfterEose`)
|
||||
* so newly-arrived label events get their target posts fetched on the next pass.
|
||||
*/
|
||||
class HashtagLabelSubAssembler(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<HashtagQueryState>,
|
||||
) : PerUniqueIdEoseManager<HashtagQueryState, String>(client, allKeys, invalidateAfterEose = true) {
|
||||
override fun updateFilter(
|
||||
key: HashtagQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> = filterHashtagLabels(key.account, key.lowercaseHashtag, key.relays, since)
|
||||
|
||||
/**
|
||||
* Only one key per hashtag. Prefixed so it doesn't collide with the main feed sub-assembler.
|
||||
*/
|
||||
override fun id(key: HashtagQueryState) = "label-" + key.lowercaseHashtag
|
||||
}
|
||||
+1
-1
@@ -49,7 +49,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.interestSets.InterestSet
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestSets.InterestSet
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.interestSets.InterestSet
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestSets.InterestSet
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
|
||||
+1
-1
@@ -27,8 +27,8 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestSets.InterestSet
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.interestSets.InterestSet
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Stable
|
||||
|
||||
+1
-1
@@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.ui.notifications.Card
|
||||
import com.vitorpamplona.amethyst.commons.util.firstFullCharOrEmoji
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.service.countToHumanReadable
|
||||
import com.vitorpamplona.amethyst.commons.util.countToHumanReadable
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font10SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10Modifier
|
||||
|
||||
+2
-2
@@ -36,8 +36,8 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.service.countToHumanReadable
|
||||
import com.vitorpamplona.amethyst.service.countToHumanReadableBytes
|
||||
import com.vitorpamplona.amethyst.commons.util.countToHumanReadable
|
||||
import com.vitorpamplona.amethyst.commons.util.countToHumanReadableBytes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
+27
-14
@@ -115,6 +115,7 @@ fun CashuWalletScreen(
|
||||
val discovering by viewModel.discovering.collectAsState()
|
||||
val mints by viewModel.mints.collectAsState()
|
||||
val balanceSats by viewModel.balanceSats.collectAsState()
|
||||
val mintBalances by viewModel.mintBalances.collectAsState()
|
||||
val history by viewModel.history.collectAsState()
|
||||
val pendingQuotes by viewModel.pendingQuotes.collectAsState()
|
||||
|
||||
@@ -158,6 +159,7 @@ fun CashuWalletScreen(
|
||||
modifier = Modifier.padding(padding),
|
||||
balanceSats = balanceSats,
|
||||
mints = mints,
|
||||
mintBalances = mintBalances,
|
||||
history = history,
|
||||
pendingQuoteCount = pendingQuotes.size,
|
||||
accountViewModel = accountViewModel,
|
||||
@@ -166,7 +168,7 @@ fun CashuWalletScreen(
|
||||
onSendLn = { sendLnOpen = true },
|
||||
onSendToken = { sendTokenOpen = true },
|
||||
onRedeem = { redeemOpen = true },
|
||||
onRecommendMint = { viewModel.recommendMint(it) },
|
||||
onTopUpMint = { nav.nav(Route.TopUpMint(it)) },
|
||||
onResumePendingQuote = {
|
||||
pendingQuotes.firstOrNull()?.let {
|
||||
viewModel.resumeMintQuote(it)
|
||||
@@ -295,6 +297,7 @@ private fun CashuWalletContent(
|
||||
modifier: Modifier,
|
||||
balanceSats: Long,
|
||||
mints: List<String>,
|
||||
mintBalances: Map<String, Long>,
|
||||
history: List<CashuSpendingHistoryEvent>,
|
||||
pendingQuoteCount: Int,
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -303,7 +306,7 @@ private fun CashuWalletContent(
|
||||
onSendLn: () -> Unit,
|
||||
onSendToken: () -> Unit,
|
||||
onRedeem: () -> Unit,
|
||||
onRecommendMint: (String) -> Unit,
|
||||
onTopUpMint: (String) -> Unit,
|
||||
onResumePendingQuote: () -> Unit,
|
||||
) {
|
||||
LazyColumn(
|
||||
@@ -340,7 +343,7 @@ private fun CashuWalletContent(
|
||||
)
|
||||
}
|
||||
items(mints, key = { it }) { mint ->
|
||||
MintRow(mint = mint, onRecommend = { onRecommendMint(mint) })
|
||||
MintRow(mint = mint, balanceSats = mintBalances[mint] ?: 0L, onTopUp = { onTopUpMint(mint) })
|
||||
}
|
||||
|
||||
if (history.isNotEmpty()) {
|
||||
@@ -526,14 +529,19 @@ private fun ActionTile(
|
||||
@Composable
|
||||
private fun MintRow(
|
||||
mint: String,
|
||||
onRecommend: (() -> Unit)? = null,
|
||||
balanceSats: Long,
|
||||
onTopUp: () -> Unit,
|
||||
) {
|
||||
val formattedBalance =
|
||||
remember(balanceSats) {
|
||||
NumberFormat.getIntegerInstance().format(balanceSats)
|
||||
}
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
modifier = Modifier.padding(start = 12.dp, end = 4.dp, top = 6.dp, bottom = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Material3Icon(
|
||||
@@ -543,15 +551,20 @@ private fun MintRow(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(text = mint, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||
if (onRecommend != null) {
|
||||
IconButton(onClick = onRecommend, modifier = Modifier.size(28.dp)) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.ThumbUp,
|
||||
contentDescription = stringRes(R.string.cashu_recommend_mint),
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "$formattedBalance ${stringRes(R.string.wallet_sats)}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
IconButton(onClick = onTopUp) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.AddCircle,
|
||||
contentDescription = stringRes(R.string.topup_mint_action),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -146,6 +146,7 @@ class CashuWalletViewModel : ViewModel() {
|
||||
val walletEvent get() = state.walletEvent
|
||||
val mints get() = state.mints
|
||||
val balanceSats get() = state.balanceSats
|
||||
val mintBalances get() = state.mintBalances
|
||||
val tokenEntries: StateFlow<List<TokenEntry>> get() = state.tokenEntries
|
||||
val history get() = state.history
|
||||
val pendingQuotes get() = state.pendingQuotes
|
||||
|
||||
-22
@@ -58,9 +58,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -154,9 +152,6 @@ fun OnchainSection(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
AddressBlock(address = address)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
ActionRow(address = address, accountViewModel = accountViewModel, orange = orange)
|
||||
}
|
||||
@@ -337,23 +332,6 @@ private fun BalanceBlock(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddressBlock(address: String) {
|
||||
Text(
|
||||
text = "Your Taproot address",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = address,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionRow(
|
||||
address: String,
|
||||
|
||||
+4
-4
@@ -99,9 +99,9 @@ fun navigateToReloadMint(
|
||||
nav.nav(Route.ReloadMint(uid))
|
||||
}
|
||||
|
||||
private fun shortMint(url: String): String = url.removePrefix("https://").removePrefix("http://").removeSuffix("/")
|
||||
internal fun shortMint(url: String): String = url.removePrefix("https://").removePrefix("http://").removeSuffix("/")
|
||||
|
||||
private fun sats(value: Long): String = showAmount(value.toBigDecimal().setScale(1))
|
||||
internal fun sats(value: Long): String = showAmount(value.toBigDecimal().setScale(1))
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -359,7 +359,7 @@ fun ReloadMintScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionHeader(text: String) {
|
||||
internal fun SectionHeader(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
@@ -368,7 +368,7 @@ private fun SectionHeader(text: String) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SourceRow(
|
||||
internal fun SourceRow(
|
||||
source: ReloadSource,
|
||||
selected: Boolean,
|
||||
onSelect: () -> Unit,
|
||||
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.Cashu
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.ui.components.util.setText
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.material3.Icon as Material3Icon
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TopUpMintScreen(
|
||||
mintUrl: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val viewModel: TopUpMintViewModel = viewModel()
|
||||
LaunchedEffect(mintUrl) { viewModel.init(accountViewModel, mintUrl) }
|
||||
|
||||
val ui by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val clipboard = LocalClipboard.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var amountText by remember(mintUrl) { mutableStateOf("") }
|
||||
|
||||
if (ui.status is TopUpStatus.Done) {
|
||||
LaunchedEffect(Unit) { nav.popBack() }
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringRes(R.string.topup_mint_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { nav.popBack() }) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.AutoMirrored.ArrowBack,
|
||||
contentDescription = stringRes(R.string.back),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(padding)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 20.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
// ── Header: the mint we're topping up + its current balance ────
|
||||
Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Material3Icon(
|
||||
imageVector = CustomHashTagIcons.Cashu,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(26.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = shortMint(ui.targetMint),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.reload_mint_available, sats(ui.targetBalanceSats)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Amount to add ─────────────────────────────────────────────
|
||||
SectionHeader(stringRes(R.string.topup_mint_amount_label))
|
||||
OutlinedTextField(
|
||||
value = amountText,
|
||||
onValueChange = { input ->
|
||||
val digits = input.filter { it.isDigit() }.take(12)
|
||||
amountText = digits
|
||||
viewModel.setAmount(digits.toLongOrNull() ?: 0L)
|
||||
},
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
suffix = { Text(stringRes(R.string.sats)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
// ── Funds from ────────────────────────────────────────────────
|
||||
SectionHeader(stringRes(R.string.reload_mint_section_from))
|
||||
ui.sources.forEach { source ->
|
||||
SourceRow(
|
||||
source = source,
|
||||
selected = source == ui.selectedSource,
|
||||
onSelect = { viewModel.selectSource(source) },
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
|
||||
when (val status = ui.status) {
|
||||
is TopUpStatus.Working -> {
|
||||
LinearProgressIndicator(progress = { status.progress }, modifier = Modifier.fillMaxWidth())
|
||||
Text(status.step, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.placeholderText)
|
||||
}
|
||||
|
||||
is TopUpStatus.AwaitingInvoice -> {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
CircularProgressIndicator(modifier = Modifier.height(20.dp))
|
||||
Text(stringRes(R.string.reload_mint_awaiting_payment), style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { scope.launch { clipboard.setText(status.invoice) } },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(stringRes(R.string.reload_mint_copy_invoice))
|
||||
}
|
||||
}
|
||||
|
||||
is TopUpStatus.Failed -> {
|
||||
Text(status.message, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodyMedium)
|
||||
Button(onClick = { viewModel.confirm() }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(stringRes(R.string.reload_mint_retry))
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
val source = ui.selectedSource
|
||||
val enabled =
|
||||
ui.amountSats > 0 &&
|
||||
(
|
||||
source is ReloadSource.LightningWallet ||
|
||||
source is ReloadSource.LightningExternal ||
|
||||
(source is ReloadSource.Mint && source.canCover)
|
||||
)
|
||||
Button(
|
||||
onClick = { viewModel.confirm() },
|
||||
enabled = enabled,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(stringRes(R.string.topup_mint_confirm))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.nip60Cashu.CashuWalletState
|
||||
import com.vitorpamplona.amethyst.model.nip60Cashu.describeMintError
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Presenter for the standalone "Top up a mint" screen.
|
||||
*
|
||||
* Reuses the same funding primitives as [ReloadMintViewModel] —
|
||||
* [CashuWalletState.rebalance] for a mint-to-mint move and
|
||||
* [com.vitorpamplona.amethyst.model.nip60Cashu.CashuWalletOps.startMintFromLightning] /
|
||||
* `completeMintFromLightning` for an LN top-up — but without any of the
|
||||
* zap-specific machinery (recipient, shared-mint intersection, fixed send
|
||||
* amount + shortfall, terminal nutzap, fund-then-send atomicity). The user
|
||||
* picks an amount and a source; we add ecash to a fixed target mint and pop.
|
||||
*/
|
||||
class TopUpMintViewModel : ViewModel() {
|
||||
private var accountViewModel: AccountViewModel? = null
|
||||
private var targetMint: String = ""
|
||||
private val state: CashuWalletState? get() = accountViewModel?.account?.cashuWalletState
|
||||
|
||||
/** The in-flight pipeline, cancelled when the screen leaves (it runs on the
|
||||
* long-lived AccountViewModel scope, not this VM's). */
|
||||
private var job: Job? = null
|
||||
|
||||
private val _uiState = MutableStateFlow(TopUpUiState())
|
||||
val uiState: StateFlow<TopUpUiState> = _uiState.asStateFlow()
|
||||
|
||||
fun init(
|
||||
accountViewModel: AccountViewModel,
|
||||
mintUrl: String,
|
||||
) {
|
||||
if (this.accountViewModel != null) return // already initialized
|
||||
this.accountViewModel = accountViewModel
|
||||
this.targetMint = mintUrl
|
||||
_uiState.update { it.copy(targetMint = mintUrl) }
|
||||
|
||||
// Wallet proofs/mints arrive from relays and can land after the screen
|
||||
// opens, so keep the source list + target balance in sync as the wallet
|
||||
// fills in. Project to the per-mint balance map and distinctUntilChanged
|
||||
// so unrelated wallet churn doesn't re-run the rebuild on every emission.
|
||||
val st = state ?: return
|
||||
viewModelScope.launch {
|
||||
combine(st.tokenEntries, st.mints) { _, _ -> st.peekMintBalances() }
|
||||
.distinctUntilChanged()
|
||||
.collect { rebuild(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Rebuild target balance + funding sources from the current wallet state,
|
||||
* keeping the user's amount and source pick intact. */
|
||||
private fun rebuild(balances: Map<String, Long>) {
|
||||
_uiState.update { cur -> recompute(cur.copy(targetBalanceSats = balances[targetMint] ?: 0L), balances) }
|
||||
}
|
||||
|
||||
/** Recompute fee estimate + sources + a default source for the current amount. */
|
||||
private fun recompute(
|
||||
base: TopUpUiState,
|
||||
balances: Map<String, Long>,
|
||||
): TopUpUiState {
|
||||
val moveSats = base.amountSats
|
||||
// Rough fee cushion for *enabling* a mint source — the real Lightning
|
||||
// feeReserve is only known once rebalance() fetches the melt quote, so this
|
||||
// is a heuristic (1%, min 2 sat). A source that clears the estimate but not
|
||||
// the real quote fails recoverably without moving funds.
|
||||
val estFee = if (moveSats <= 0L) 0L else (moveSats / 100L).coerceAtLeast(2L)
|
||||
val needFromSource = moveSats + estFee
|
||||
|
||||
val mintSources =
|
||||
balances
|
||||
.filterKeys { it != targetMint }
|
||||
.entries
|
||||
.sortedByDescending { it.value }
|
||||
.map { ReloadSource.Mint(it.key, it.value, canCover = moveSats <= 0L || it.value >= needFromSource) }
|
||||
|
||||
val lightningSources: List<ReloadSource> =
|
||||
nwcWallets()
|
||||
.map { ReloadSource.LightningWallet(it.id, it.name) }
|
||||
.ifEmpty { listOf(ReloadSource.LightningExternal) }
|
||||
val sources = mintSources + lightningSources
|
||||
|
||||
// Keep the current pick if still valid; else default to the richest mint that
|
||||
// can cover (no new sats), else the configured/first Lightning wallet.
|
||||
val keep =
|
||||
when (val prev = base.selectedSource) {
|
||||
is ReloadSource.Mint -> mintSources.firstOrNull { it.mintUrl == prev.mintUrl && it.canCover }
|
||||
is ReloadSource.LightningWallet -> lightningSources.firstOrNull { it is ReloadSource.LightningWallet && it.walletId == prev.walletId }
|
||||
ReloadSource.LightningExternal -> lightningSources.firstOrNull { it is ReloadSource.LightningExternal }
|
||||
null -> null
|
||||
}
|
||||
val selectedSource: ReloadSource? =
|
||||
keep
|
||||
?: mintSources.filter { it.canCover }.maxByOrNull { it.balanceSats }
|
||||
?: defaultLightningSource(lightningSources)
|
||||
|
||||
return base.copy(
|
||||
estFeeSats = estFee,
|
||||
sources = sources.toImmutableList(),
|
||||
selectedSource = selectedSource,
|
||||
)
|
||||
}
|
||||
|
||||
fun selectSource(source: ReloadSource) {
|
||||
_uiState.update { it.copy(selectedSource = source) }
|
||||
}
|
||||
|
||||
fun setAmount(sats: Long) {
|
||||
_uiState.update { recompute(it.copy(amountSats = sats.coerceAtLeast(0L)), currentBalances()) }
|
||||
}
|
||||
|
||||
private fun currentBalances(): Map<String, Long> = state?.peekMintBalances().orEmpty()
|
||||
|
||||
fun confirm() {
|
||||
val vm = accountViewModel ?: return
|
||||
val s = _uiState.value
|
||||
val source = s.selectedSource ?: return
|
||||
val moveSats = s.amountSats
|
||||
if (moveSats <= 0L) return
|
||||
|
||||
// In-flight guard: only start from a resting state so a double tap (or the
|
||||
// Failed-state retry) can't launch a second pipeline.
|
||||
if (s.status !is TopUpStatus.Configuring && s.status !is TopUpStatus.Failed) return
|
||||
setStatus(TopUpStatus.Working("Starting", 0.05f))
|
||||
|
||||
job?.cancel()
|
||||
job =
|
||||
vm.launchSigner {
|
||||
try {
|
||||
when (source) {
|
||||
is ReloadSource.Mint -> topUpFromMint(source.mintUrl, moveSats)
|
||||
is ReloadSource.LightningWallet -> topUpFromLightning(moveSats, walletUriFor(source.walletId))
|
||||
ReloadSource.LightningExternal -> topUpFromLightning(moveSats, null)
|
||||
}
|
||||
setStatus(TopUpStatus.Done)
|
||||
} catch (e: CancellationException) {
|
||||
throw e // screen left mid-flow — don't mask as a Failed state
|
||||
} catch (e: Exception) {
|
||||
setStatus(TopUpStatus.Failed(describeMintError(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun topUpFromMint(
|
||||
sourceMint: String,
|
||||
moveSats: Long,
|
||||
) {
|
||||
val st = state ?: return
|
||||
setStatus(TopUpStatus.Working("Moving funds", 0.1f))
|
||||
st.rebalance(
|
||||
sourceMintUrl = sourceMint,
|
||||
targetMintUrl = targetMint,
|
||||
sats = moveSats,
|
||||
onProgress = { p -> setStatus(TopUpStatus.Working("Moving funds", p.coerceIn(0.1f, 0.95f))) },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun topUpFromLightning(
|
||||
moveSats: Long,
|
||||
walletUri: Nip47WalletConnect.Nip47URINorm?,
|
||||
) {
|
||||
val vm = accountViewModel ?: return
|
||||
val st = state ?: return
|
||||
val ops = st.ops
|
||||
|
||||
setStatus(TopUpStatus.Working("Requesting invoice", 0.1f))
|
||||
val flow = ops.startMintFromLightning(targetMint, moveSats)
|
||||
|
||||
if (walletUri != null) {
|
||||
setStatus(TopUpStatus.Working("Paying from your wallet", 0.35f))
|
||||
// Fire-and-forget: the mint-quote poll below is the source of truth for
|
||||
// whether the payment actually landed.
|
||||
runCatching {
|
||||
vm.account.sendNwcRequestToWallet(walletUri, PayInvoiceMethod.create(flow.invoice)) { }
|
||||
}
|
||||
} else {
|
||||
// No NWC — surface the invoice for an external wallet and keep polling.
|
||||
setStatus(TopUpStatus.AwaitingInvoice(flow.invoice, moveSats))
|
||||
}
|
||||
|
||||
// External payment can take a while; the poll runs on a job tied to the
|
||||
// screen (cancelled in onCleared), so leaving stops it.
|
||||
val attempts = 90
|
||||
val delayMs = 2_000L
|
||||
var paid = false
|
||||
var attempt = 0
|
||||
while (!paid && attempt < attempts) {
|
||||
paid = ops.checkMintQuote(targetMint, flow.mintQuote.quote).isSettled()
|
||||
if (!paid) {
|
||||
delay(delayMs)
|
||||
attempt++
|
||||
}
|
||||
}
|
||||
if (!paid) {
|
||||
setStatus(TopUpStatus.Failed("Invoice not paid yet — you can finish it later from the pending quote banner"))
|
||||
return
|
||||
}
|
||||
setStatus(TopUpStatus.Working("Issuing ecash", 0.85f))
|
||||
ops.completeMintFromLightning(targetMint, flow.quoteEvent, moveSats)
|
||||
}
|
||||
|
||||
private fun setStatus(status: TopUpStatus) {
|
||||
_uiState.update { it.copy(status = status) }
|
||||
}
|
||||
|
||||
private fun nwcWallets() =
|
||||
accountViewModel
|
||||
?.account
|
||||
?.settings
|
||||
?.nwcWallets
|
||||
?.value
|
||||
.orEmpty()
|
||||
|
||||
private fun walletUriFor(walletId: String) = nwcWallets().firstOrNull { it.id == walletId }?.uri
|
||||
|
||||
/** The configured default NWC wallet's source, else the first Lightning source. */
|
||||
private fun defaultLightningSource(lightning: List<ReloadSource>): ReloadSource? {
|
||||
val defaultId =
|
||||
accountViewModel
|
||||
?.account
|
||||
?.settings
|
||||
?.defaultNwcWallet()
|
||||
?.id
|
||||
return lightning.firstOrNull { it is ReloadSource.LightningWallet && it.walletId == defaultId }
|
||||
?: lightning.firstOrNull()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
// The pipeline runs on the AccountViewModel scope, not this VM's, so it would
|
||||
// outlive the screen — cancel it when the screen goes away.
|
||||
job?.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
||||
|
||||
/** Progress of a standalone mint top-up. Mirrors [ReloadStatus] minus the zap step. */
|
||||
@Immutable
|
||||
sealed interface TopUpStatus {
|
||||
data object Configuring : TopUpStatus
|
||||
|
||||
data class Working(
|
||||
val step: String,
|
||||
val progress: Float,
|
||||
) : TopUpStatus
|
||||
|
||||
/** External Lightning — the user must pay [invoice]; we keep polling. */
|
||||
data class AwaitingInvoice(
|
||||
val invoice: String,
|
||||
val sats: Long,
|
||||
) : TopUpStatus
|
||||
|
||||
data object Done : TopUpStatus
|
||||
|
||||
data class Failed(
|
||||
val message: String,
|
||||
) : TopUpStatus
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class TopUpUiState(
|
||||
val targetMint: String = "",
|
||||
val targetBalanceSats: Long = 0L,
|
||||
/** The amount to add to the target mint. */
|
||||
val amountSats: Long = 0L,
|
||||
val estFeeSats: Long = 0L,
|
||||
val sources: ImmutableList<ReloadSource> = persistentListOf(),
|
||||
val selectedSource: ReloadSource? = null,
|
||||
val status: TopUpStatus = TopUpStatus.Configuring,
|
||||
)
|
||||
+31
-44
@@ -21,8 +21,10 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -34,8 +36,9 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
@@ -61,6 +64,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.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -76,10 +81,6 @@ import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayDragState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.draggableRelayItem
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relayDragHandle
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.rememberRelayDragState
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.NumberFormat
|
||||
@@ -221,12 +222,6 @@ private fun MultiWalletHomeContent(
|
||||
}
|
||||
}
|
||||
|
||||
val dragState =
|
||||
rememberRelayDragState(
|
||||
onMove = { from, to -> walletViewModel.moveWallet(from, to) },
|
||||
itemCount = { walletInfoList.size },
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier =
|
||||
@@ -234,17 +229,10 @@ private fun MultiWalletHomeContent(
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
userScrollEnabled = !dragState.isDragging,
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
itemsIndexed(walletInfoList, key = { _, info -> info.walletId }) { index, walletInfo ->
|
||||
items(walletInfoList, key = { info -> info.walletId }) { walletInfo ->
|
||||
WalletCard(
|
||||
walletInfo = walletInfo,
|
||||
index = index,
|
||||
dragState = if (walletInfoList.size > 1) dragState else null,
|
||||
onSelect = {
|
||||
walletViewModel.selectWallet(walletInfo.walletId)
|
||||
nav.nav(Route.WalletDetail(walletInfo.walletId))
|
||||
@@ -293,8 +281,6 @@ private fun MultiWalletHomeContent(
|
||||
@Composable
|
||||
private fun WalletCard(
|
||||
walletInfo: WalletInfo,
|
||||
index: Int,
|
||||
dragState: RelayDragState?,
|
||||
onSelect: () -> Unit,
|
||||
onSetDefault: () -> Unit,
|
||||
onRename: (String) -> Unit,
|
||||
@@ -339,7 +325,6 @@ private fun WalletCard(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.let { if (dragState != null) it.draggableRelayItem(index, dragState) else it }
|
||||
.clickable(onClick = onSelect),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
border =
|
||||
@@ -366,22 +351,6 @@ private fun WalletCard(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
// Drag handle leads the row — same convention as the
|
||||
// relay reorder list — so a center-aligned icon doesn't
|
||||
// land between the balance amount and its "sats" label.
|
||||
if (dragState != null) {
|
||||
Icon(
|
||||
MaterialSymbols.DragIndicator,
|
||||
contentDescription = stringRes(R.string.wallet_reorder),
|
||||
modifier =
|
||||
Modifier
|
||||
.size(24.dp)
|
||||
.relayDragHandle(index, dragState),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
@@ -489,6 +458,29 @@ private fun WalletCard(
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors OnchainSection's BitcoinChip so both payment rails render their
|
||||
// logo the same way — a filled circular chip with a white glyph — instead of
|
||||
// one being a bare tinted vector. Cashu uses the theme primary in place of
|
||||
// bitcoin orange to keep the two rails distinct at a glance.
|
||||
@Composable
|
||||
private fun CashuChip() {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.size(28.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color = MaterialTheme.colorScheme.primary),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Material3Icon(
|
||||
imageVector = CustomHashTagIcons.Cashu,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CashuWalletRow(
|
||||
balanceSats: Long,
|
||||
@@ -512,12 +504,7 @@ private fun CashuWalletRow(
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Material3Icon(
|
||||
imageVector = CustomHashTagIcons.Cashu,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(28.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
CashuChip()
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
|
||||
+1
-10
@@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.NwcWalletEntryNorm
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcWalletEntryNorm
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
@@ -296,15 +296,6 @@ class WalletViewModel : ViewModel() {
|
||||
refreshWalletList()
|
||||
}
|
||||
|
||||
fun moveWallet(
|
||||
fromIndex: Int,
|
||||
toIndex: Int,
|
||||
) {
|
||||
val acc = account ?: return
|
||||
acc.settings.moveNwcWallet(fromIndex, toIndex)
|
||||
refreshWalletList()
|
||||
}
|
||||
|
||||
fun selectWallet(walletId: String) {
|
||||
_selectedWalletId.value = walletId
|
||||
_balanceSats.value = walletInfoMap.value[walletId]?.balanceSats
|
||||
|
||||
@@ -747,6 +747,17 @@
|
||||
<string name="articles">Články</string>
|
||||
<string name="route_music_tracks">Hudba</string>
|
||||
<string name="route_music_playlists">Playlisty</string>
|
||||
<string name="route_podcast_episodes">Epizody</string>
|
||||
<string name="route_podcasts">Podcasty</string>
|
||||
<string name="podcast_view_episodes">Zobrazit epizody</string>
|
||||
<string name="podcast_episodes_section">Epizody</string>
|
||||
<string name="podcast_no_episodes">Zatím nenalezeny žádné epizody</string>
|
||||
<plurals name="podcast_episode_count">
|
||||
<item quantity="one">%1$d epizoda</item>
|
||||
<item quantity="few">%1$d epizody</item>
|
||||
<item quantity="many">%1$d epizody</item>
|
||||
<item quantity="other">%1$d epizod</item>
|
||||
</plurals>
|
||||
<string name="new_music_track">Nová hudební stopa</string>
|
||||
<string name="new_music_playlist">Nový playlist</string>
|
||||
<string name="music_track_title_label">Název</string>
|
||||
@@ -781,6 +792,11 @@
|
||||
<string name="add_to_public_bookmarks">Přidat do veřejných záložek</string>
|
||||
<string name="remove_from_private_bookmarks">Odebrat ze soukromých záložek</string>
|
||||
<string name="remove_from_public_bookmarks">Odebrat z veřejných záložek</string>
|
||||
<string name="add_hashtag_label">Přidat hashtag</string>
|
||||
<string name="add_hashtag_label_title">Přidat hashtag</string>
|
||||
<string name="add_hashtag_label_explainer">Veřejně označte tento příspěvek hashtagem (štítek NIP-32). Lidé, kteří vás sledují, jej uvidí ve feedu daného hashtagu.</string>
|
||||
<string name="add_hashtag_label_confirm">Přidat</string>
|
||||
<string name="hashtag_label_added_by">přidal(a)</string>
|
||||
<string name="pinned_notes">Připnuté poznámky</string>
|
||||
<string name="pinned_notes_explainer">Vaše připnuté poznámky</string>
|
||||
<string name="pin_to_profile">Připnout na profil</string>
|
||||
@@ -849,6 +865,28 @@
|
||||
<string name="quick_zap_amounts">Rychlé částky Zap</string>
|
||||
<string name="quick_zap_amounts_explainer">Zobrazí se při stisknutí tlačítka zap. Klepnutím na částku ji odeberete. Pokud ponecháte prázdné, otevře se pokaždé dialog pro zadání částky.</string>
|
||||
<string name="send_onchain_instead">Odeslat on-chain</string>
|
||||
<string name="reload_mint_title">Dobít mint</string>
|
||||
<string name="reload_mint_topup_label">Částka dobití</string>
|
||||
<string name="reload_mint_section_to">Mint k dobití</string>
|
||||
<string name="reload_mint_section_from">Prostředky z</string>
|
||||
<string name="reload_mint_pay_lightning">Zaplatit pomocí Lightning</string>
|
||||
<string name="reload_mint_lightning_desc">Vydejte nový ecash z vaší Lightning peněženky</string>
|
||||
<string name="reload_mint_not_enough">Tady není dost</string>
|
||||
<string name="reload_mint_available">%1$s sats k dispozici</string>
|
||||
<string name="reload_mint_confirm">Dobít a poslat zap</string>
|
||||
<string name="reload_mint_send_confirm">Poslat zap</string>
|
||||
<string name="reload_mint_awaiting_payment">Čekám na platbu…</string>
|
||||
<string name="reload_mint_retry">Zkusit znovu</string>
|
||||
<string name="reload_mint_summary">Dobít %1$s sats do %2$s a poslat %3$s zap %4$s sats · poplatek ≈ %5$s sats</string>
|
||||
<string name="reload_mint_summary_funded">Poslat zap %1$s %2$s sats z %3$s</string>
|
||||
<string name="reload_mint_recipient_fallback">příjemce</string>
|
||||
<string name="reload_mint_needs_more">chybí %1$s sat</string>
|
||||
<string name="reload_mint_funded">financováno</string>
|
||||
<string name="reload_mint_copy_invoice">Kopírovat fakturu</string>
|
||||
<string name="topup_mint_title">Dobít mint</string>
|
||||
<string name="topup_mint_action">Dobít tento mint</string>
|
||||
<string name="topup_mint_amount_label">Částka k přidání</string>
|
||||
<string name="topup_mint_confirm">Dobít</string>
|
||||
<string name="zap_privacy_section">Soukromí Zap</string>
|
||||
<string name="zap_type_section_explainer">Určuje, jak je zobrazena vaše identita při odesílání zapsu.</string>
|
||||
<string name="wallet_connect_connect_app">Připojit peněženku</string>
|
||||
@@ -1733,6 +1771,99 @@
|
||||
<string name="wallet_no_wallets">Žádné peněženky nejsou připojeny</string>
|
||||
<string name="wallet_no_wallets_description">Připojte jednu nebo více NWC peněženek pro odesílání a přijímání plateb.</string>
|
||||
<string name="wallet_add_connection">Přidat NWC připojení</string>
|
||||
<string name="wallet_add_choose_type">Zvolte typ peněženky</string>
|
||||
<string name="wallet_add_nwc_title">Připojit Lightning peněženku (NWC)</string>
|
||||
<string name="wallet_add_nwc_description">Plaťte přes externí Lightning peněženku pomocí Nostr Wallet Connect.</string>
|
||||
<string name="wallet_add_cashu_title">Peněženka Cashu (NIP-60)</string>
|
||||
<string name="wallet_add_cashu_description">Ecash peněženka v aplikaci založená na mintu Cashu. Tokeny jsou uloženy zašifrované na relayích.</string>
|
||||
<string name="cashu_wallet_title">Peněženka Cashu</string>
|
||||
<string name="cashu_no_wallet">Zatím žádná peněženka Cashu</string>
|
||||
<string name="cashu_no_wallet_description">Vytvořte si peněženku Cashu pro uchovávání tokenů ecash a příjem nutzapů.</string>
|
||||
<string name="cashu_discovering">Hledám vaši peněženku…</string>
|
||||
<string name="cashu_discovering_description">Peněženky NIP-60 se synchronizují mezi klienty. Pokud jste si nějakou vytvořili v jiné aplikaci pod tímto klíčem Nostr, měla by se za pár sekund objevit.</string>
|
||||
<string name="cashu_balance">Zůstatek</string>
|
||||
<string name="cashu_mints">Minty</string>
|
||||
<string name="cashu_mint_url">URL mintu</string>
|
||||
<string name="cashu_remove_mint">Odebrat mint</string>
|
||||
<string name="cashu_history">Historie</string>
|
||||
<string name="cashu_edit_wallet">Upravit peněženku</string>
|
||||
<string name="cashu_p2pk_section">Klíč pro nutzap (pokročilé)</string>
|
||||
<string name="cashu_p2pk_explainer">Samostatný soukromý klíč používaný pouze k příjmu nutzapů NIP-61. Není to klíč vaší identity Nostr.</string>
|
||||
<string name="cashu_p2pk_autogen">Vygenerovat nový klíč</string>
|
||||
<string name="cashu_p2pk_autogen_warning_edit">Zneplatní všechny čekající nutzapy uzamčené ke stávajícímu klíči.</string>
|
||||
<string name="cashu_p2pk_keep_current">Ponechat stávající klíč</string>
|
||||
<string name="cashu_p2pk_manual_label">Soukromý klíč P2PK (hex)</string>
|
||||
<string name="wallet_edit_cashu_title">Upravit peněženku Cashu</string>
|
||||
<string name="wallet_save_changes">Uložit změny</string>
|
||||
<string name="cashu_mint_no_recs">Zatím žádná doporučení</string>
|
||||
<string name="cashu_mint_recommended_by">Doporučuje</string>
|
||||
<plurals name="cashu_mint_plus_others">
|
||||
<item quantity="one">+%1$d další</item>
|
||||
<item quantity="few">+%1$d další</item>
|
||||
<item quantity="many">+%1$d dalšího</item>
|
||||
<item quantity="other">+%1$d dalších</item>
|
||||
</plurals>
|
||||
<plurals name="cashu_mint_recommended_by_others">
|
||||
<item quantity="one">Doporučuje %1$d další</item>
|
||||
<item quantity="few">Doporučují %1$d další</item>
|
||||
<item quantity="many">Doporučuje %1$d dalšího</item>
|
||||
<item quantity="other">Doporučuje %1$d dalších</item>
|
||||
</plurals>
|
||||
<string name="cashu_mint_popular_header">Oblíbené minty</string>
|
||||
<string name="cashu_mint_matches_header">Odpovídající minty</string>
|
||||
<string name="cashu_recommend_mint">Doporučit tento mint</string>
|
||||
<string name="cashu_action_receive">Přijmout</string>
|
||||
<string name="cashu_action_send_ln">Poslat LN</string>
|
||||
<string name="cashu_action_send_token">Poslat token</string>
|
||||
<string name="cashu_action_redeem">Uplatnit</string>
|
||||
<string name="cashu_amount_sats">Částka (sats)</string>
|
||||
<string name="cashu_pick_mint">Vyberte mint</string>
|
||||
<string name="cashu_memo_optional">Poznámka (volitelné)</string>
|
||||
<string name="cashu_invoice_bolt11">Lightning faktura (bolt11)</string>
|
||||
<string name="cashu_token_label">Token Cashu</string>
|
||||
<string name="cashu_copy_invoice">Kopírovat fakturu</string>
|
||||
<string name="cashu_request_invoice">Vyžádat fakturu</string>
|
||||
<string name="cashu_discard_invoice">Zahodit fakturu</string>
|
||||
<string name="cashu_history_incoming">Přijato</string>
|
||||
<string name="cashu_history_outgoing">Odesláno</string>
|
||||
<string name="cashu_settings_title">Nastavení peněženky Cashu</string>
|
||||
<string name="cashu_settings_edit_wallet">Upravit údaje peněženky</string>
|
||||
<string name="cashu_settings_edit_wallet_subtitle">Minty, klíč pro nutzap</string>
|
||||
<string name="cashu_settings_my_recommendations">Moje doporučení mintů</string>
|
||||
<string name="cashu_settings_no_recommendations">Zatím jste nedoporučili žádný mint. Klepnutím na palec nahoru vedle mintu jej veřejně doporučíte.</string>
|
||||
<string name="cashu_settings_delete_recommendation">Smazat doporučení</string>
|
||||
<string name="cashu_settings_delete_confirm_title">Stáhnout doporučení?</string>
|
||||
<string name="cashu_settings_delete_confirm_body">Publikovat žádost o smazání vašeho doporučení kind:38000 pro %1$s. Relaye, které dodržují NIP-09, jej odstraní.</string>
|
||||
<string name="cashu_settings_add_recommendation">Doporučit mint</string>
|
||||
<string name="cashu_settings_restore_title">Obnovit ze seedu</string>
|
||||
<string name="cashu_settings_restore_subtitle">Znovu odvoďte každé minulé tajemství a požádejte každý mint o vrácení všech slepých podpisů, které stále eviduje. Užitečné po ztrátě zařízení nebo smazání vašich token událostí podvodným relayem.</string>
|
||||
<string name="cashu_settings_restore_running">Prohledávám minty…</string>
|
||||
<string name="cashu_settings_restore_nothing_found">Nenalezeny žádné obnovitelné proofy.</string>
|
||||
<string name="cashu_settings_restore_result">Obnoveno %1$s sats z %2$s proofů.</string>
|
||||
<string name="cashu_pay_invoice">Zaplatit fakturu</string>
|
||||
<string name="cashu_get_quote">Získat nabídku</string>
|
||||
<string name="cashu_getting_quote">Žádám mint o nabídku…</string>
|
||||
<string name="cashu_quote_confirm">Zaplatit %1$s sat + až %2$s sat na poplatcích?</string>
|
||||
<string name="cashu_verify">Ověřit</string>
|
||||
<string name="cashu_mint_reachable">✓ Mint je dostupný</string>
|
||||
<string name="cashu_mint_unreachable">Nelze se připojit k mintu: %1$s</string>
|
||||
<string name="nutzap_failed_title">Nutzap selhal</string>
|
||||
<string name="nutzap_failed_no_recipient">V poznámce chybí veřejný klíč příjemce</string>
|
||||
<string name="nutzap_failed_no_event">Nelze sestavit odkaz na událost</string>
|
||||
<string name="cashu_create_token">Vytvořit token</string>
|
||||
<string name="cashu_redeem_button">Uplatnit</string>
|
||||
<string name="cashu_done">Hotovo</string>
|
||||
<string name="cashu_requesting_invoice">Žádám mint o fakturu…</string>
|
||||
<string name="cashu_waiting_for_payment">Čekám na zaplacení faktury…</string>
|
||||
<string name="cashu_completing_mint">Vydávám proofy…</string>
|
||||
<string name="cashu_paying_invoice">Platím přes mint…</string>
|
||||
<string name="cashu_building_token">Vyměňuji proofy…</string>
|
||||
<string name="cashu_redeeming">Vyměňuji proofy do vaší peněženky…</string>
|
||||
<string name="cashu_receive_invoice_explainer">Zaplaťte tuto fakturu pro vydání %1$s sats v ecash.</string>
|
||||
<string name="cashu_received_amount">Přijato %1$s sats.</string>
|
||||
<string name="cashu_paid_amount">Zaplaceno %1$s sats (poplatky: %2$s sats).</string>
|
||||
<string name="cashu_token_ready">Token na %1$s sats je připraven. Sdílejte jej s příjemcem.</string>
|
||||
<string name="cashu_redeemed_amount">Uplatněno %1$s sats do vaší peněženky.</string>
|
||||
<string name="wallet_paste_uri">Vložit URI nostr+walletconnect://</string>
|
||||
<string name="wallet_save">Uložit</string>
|
||||
<string name="wallet_rename">Přejmenovat</string>
|
||||
@@ -1741,7 +1872,6 @@
|
||||
<string name="wallet_invalid_uri">Neplatné URI NWC připojení</string>
|
||||
<string name="wallet_move_up">Posunout nahoru</string>
|
||||
<string name="wallet_move_down">Posunout dolů</string>
|
||||
<string name="wallet_reorder">Změnit pořadí peněženek</string>
|
||||
<string name="wallet_onchain_transactions">On-chain transakce</string>
|
||||
<string name="wallet_onchain_no_address">Pro tento účet není k dispozici žádná on-chain adresa.</string>
|
||||
<string name="wallet_onchain_no_backend">Není nakonfigurován žádný on-chain backend.</string>
|
||||
@@ -2342,6 +2472,10 @@
|
||||
<string name="kind_audio_track">Audio stopa</string>
|
||||
<string name="kind_music_track">Hudební stopa</string>
|
||||
<string name="kind_music_playlist">Hudební playlist</string>
|
||||
<string name="kind_podcast_episode">Epizoda podcastu</string>
|
||||
<string name="kind_podcast_metadata">Podcast</string>
|
||||
<string name="kind_authored_podcasts">Vytvořené podcasty</string>
|
||||
<string name="kind_favorite_podcasts">Oblíbené podcasty</string>
|
||||
<string name="kind_badge_awards">Ocenění odznaků</string>
|
||||
<string name="kind_badge_definitions">Definice odznaků</string>
|
||||
<string name="kind_accepted_badge_set">Přijatá sada odznaků</string>
|
||||
@@ -2596,6 +2730,14 @@
|
||||
<item quantity="many">%1$d let</item>
|
||||
<item quantity="other">%1$d let</item>
|
||||
</plurals>
|
||||
<plurals name="cashu_pending_quotes_title">
|
||||
<item quantity="one">%1$d čekající faktura</item>
|
||||
<item quantity="few">%1$d čekající faktury</item>
|
||||
<item quantity="many">%1$d čekající faktury</item>
|
||||
<item quantity="other">%1$d čekajících faktur</item>
|
||||
</plurals>
|
||||
<string name="cashu_pending_quotes_subtitle">Klepnutím pokračujte a zkontrolujte stav.</string>
|
||||
<string name="cashu_pending_quotes_resume">Pokračovat</string>
|
||||
<string name="event_sync_less_than_until"><%1$s</string>
|
||||
<string name="event_sync_status_connecting">Připojování</string>
|
||||
<string name="event_sync_status_downloading">Stahování</string>
|
||||
|
||||
@@ -737,6 +737,14 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="articles">Artikel</string>
|
||||
<string name="route_music_tracks">Musik</string>
|
||||
<string name="route_music_playlists">Wiedergabelisten</string>
|
||||
<string name="route_podcast_episodes">Folgen</string>
|
||||
<string name="podcast_view_episodes">Folgen ansehen</string>
|
||||
<string name="podcast_episodes_section">Folgen</string>
|
||||
<string name="podcast_no_episodes">Noch keine Folgen gefunden</string>
|
||||
<plurals name="podcast_episode_count">
|
||||
<item quantity="one">%1$d Folge</item>
|
||||
<item quantity="other">%1$d Folgen</item>
|
||||
</plurals>
|
||||
<string name="new_music_track">Neuer Musiktitel</string>
|
||||
<string name="new_music_playlist">Neue Playlist</string>
|
||||
<string name="music_track_title_label">Titel</string>
|
||||
@@ -769,6 +777,11 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="add_to_public_bookmarks">Zu den öffentlichen Lesezeichen hinzufügen</string>
|
||||
<string name="remove_from_private_bookmarks">Aus den privaten Lesezeichen entfernen</string>
|
||||
<string name="remove_from_public_bookmarks">Aus den öffentlichen Lesezeichen entfernen</string>
|
||||
<string name="add_hashtag_label">Hashtag hinzufügen</string>
|
||||
<string name="add_hashtag_label_title">Hashtag hinzufügen</string>
|
||||
<string name="add_hashtag_label_explainer">Markiere diesen Beitrag öffentlich mit einem Hashtag (NIP-32-Label). Wer dir folgt, sieht ihn im Feed dieses Hashtags.</string>
|
||||
<string name="add_hashtag_label_confirm">Hinzufügen</string>
|
||||
<string name="hashtag_label_added_by">hinzugefügt von</string>
|
||||
<string name="pinned_notes">Angeheftete Notizen</string>
|
||||
<string name="pinned_notes_explainer">Deine angepinnten Notizen</string>
|
||||
<string name="pin_to_profile">An Profil anheften</string>
|
||||
@@ -837,6 +850,29 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="quick_zap_amounts">Schnelle Zap-Beträge</string>
|
||||
<string name="quick_zap_amounts_explainer">Wird beim Drücken der Zap-Schaltfläche angezeigt. Tippe auf einen Betrag, um ihn zu entfernen. Wenn du es leer lässt, wird jedes Mal ein Dialog zur Eingabe eines Betrags geöffnet.</string>
|
||||
<string name="send_onchain_instead">Stattdessen On-Chain senden</string>
|
||||
<string name="reload_mint_title">Mint aufladen</string>
|
||||
<string name="reload_mint_sats_amount">%1$s Sats</string>
|
||||
<string name="reload_mint_topup_label">Aufladebetrag</string>
|
||||
<string name="reload_mint_section_to">Aufzuladender Mint</string>
|
||||
<string name="reload_mint_section_from">Mittel von</string>
|
||||
<string name="reload_mint_pay_lightning">Mit Lightning bezahlen</string>
|
||||
<string name="reload_mint_lightning_desc">Präge neues Ecash aus deiner Lightning-Wallet</string>
|
||||
<string name="reload_mint_not_enough">Hier nicht genug</string>
|
||||
<string name="reload_mint_available">%1$s Sats verfügbar</string>
|
||||
<string name="reload_mint_confirm">Aufladen & Zap senden</string>
|
||||
<string name="reload_mint_send_confirm">Zap senden</string>
|
||||
<string name="reload_mint_awaiting_payment">Warte auf Zahlung…</string>
|
||||
<string name="reload_mint_retry">Erneut versuchen</string>
|
||||
<string name="reload_mint_summary">Lade %1$s Sats zu %2$s auf und zappe %3$s %4$s Sats · Gebühr ≈ %5$s Sats</string>
|
||||
<string name="reload_mint_summary_funded">Zappe %1$s %2$s Sats von %3$s</string>
|
||||
<string name="reload_mint_recipient_fallback">der Empfänger</string>
|
||||
<string name="reload_mint_needs_more">braucht %1$s Sat mehr</string>
|
||||
<string name="reload_mint_funded">finanziert</string>
|
||||
<string name="reload_mint_copy_invoice">Rechnung kopieren</string>
|
||||
<string name="topup_mint_title">Mint aufladen</string>
|
||||
<string name="topup_mint_action">Diesen Mint aufladen</string>
|
||||
<string name="topup_mint_amount_label">Hinzuzufügender Betrag</string>
|
||||
<string name="topup_mint_confirm">Aufladen</string>
|
||||
<string name="zap_privacy_section">Zap-Datenschutz</string>
|
||||
<string name="zap_type_section_explainer">Legt fest, wie deine Identität beim Senden eines Zaps angezeigt wird.</string>
|
||||
<string name="wallet_connect_connect_app">Wallet verbinden</string>
|
||||
@@ -1716,6 +1752,94 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="wallet_no_wallets">Keine Wallets verbunden</string>
|
||||
<string name="wallet_no_wallets_description">Verbinde eine oder mehrere NWC-Wallets, um Zahlungen zu senden und zu empfangen.</string>
|
||||
<string name="wallet_add_connection">NWC-Verbindung hinzufügen</string>
|
||||
<string name="wallet_add_choose_type">Wallet-Typ wählen</string>
|
||||
<string name="wallet_add_nwc_title">Lightning-Wallet verbinden (NWC)</string>
|
||||
<string name="wallet_add_nwc_description">Bezahle über eine externe Lightning-Wallet mit Nostr Wallet Connect.</string>
|
||||
<string name="wallet_add_cashu_title">Cashu-Wallet (NIP-60)</string>
|
||||
<string name="wallet_add_cashu_description">Eine In-App-Ecash-Wallet, die von einem Cashu-Mint gestützt wird. Tokens werden verschlüsselt auf Relays gespeichert.</string>
|
||||
<string name="cashu_wallet_title">Cashu-Wallet</string>
|
||||
<string name="cashu_no_wallet">Noch keine Cashu-Wallet</string>
|
||||
<string name="cashu_no_wallet_description">Erstelle eine Cashu-Wallet, um Ecash-Tokens zu halten und Nutzaps zu empfangen.</string>
|
||||
<string name="cashu_discovering">Suche nach deiner Wallet…</string>
|
||||
<string name="cashu_discovering_description">NIP-60-Wallets werden über Clients hinweg synchronisiert. Wenn du in einer anderen App eine mit diesem Nostr-Schlüssel erstellt hast, sollte sie in wenigen Sekunden erscheinen.</string>
|
||||
<string name="cashu_balance">Guthaben</string>
|
||||
<string name="cashu_mint_url">Mint-URL</string>
|
||||
<string name="cashu_remove_mint">Mint entfernen</string>
|
||||
<string name="cashu_history">Verlauf</string>
|
||||
<string name="cashu_edit_wallet">Wallet bearbeiten</string>
|
||||
<string name="cashu_p2pk_section">Nutzap-Schlüssel (erweitert)</string>
|
||||
<string name="cashu_p2pk_explainer">Ein separater privater Schlüssel, der nur zum Empfangen von NIP-61-Nutzaps dient. Nicht dein Nostr-Identitätsschlüssel.</string>
|
||||
<string name="cashu_p2pk_autogen">Neuen Schlüssel erzeugen</string>
|
||||
<string name="cashu_p2pk_autogen_warning_edit">Macht alle ausstehenden Nutzaps ungültig, die an den aktuellen Schlüssel gebunden sind.</string>
|
||||
<string name="cashu_p2pk_keep_current">Aktuellen Schlüssel behalten</string>
|
||||
<string name="cashu_p2pk_manual_label">Privater P2PK-Schlüssel (hex)</string>
|
||||
<string name="wallet_edit_cashu_title">Cashu-Wallet bearbeiten</string>
|
||||
<string name="wallet_save_changes">Änderungen speichern</string>
|
||||
<string name="cashu_mint_no_recs">Noch keine Empfehlungen</string>
|
||||
<string name="cashu_mint_recommended_by">Empfohlen von</string>
|
||||
<plurals name="cashu_mint_plus_others">
|
||||
<item quantity="one">+%1$d weiterer</item>
|
||||
<item quantity="other">+%1$d weitere</item>
|
||||
</plurals>
|
||||
<plurals name="cashu_mint_recommended_by_others">
|
||||
<item quantity="one">Von %1$d weiterem empfohlen</item>
|
||||
<item quantity="other">Von %1$d weiteren empfohlen</item>
|
||||
</plurals>
|
||||
<string name="cashu_mint_popular_header">Beliebte Mints</string>
|
||||
<string name="cashu_mint_matches_header">Passende Mints</string>
|
||||
<string name="cashu_recommend_mint">Diesen Mint empfehlen</string>
|
||||
<string name="cashu_action_receive">Empfangen</string>
|
||||
<string name="cashu_action_send_ln">LN senden</string>
|
||||
<string name="cashu_action_send_token">Token senden</string>
|
||||
<string name="cashu_action_redeem">Einlösen</string>
|
||||
<string name="cashu_amount_sats">Betrag (Sats)</string>
|
||||
<string name="cashu_pick_mint">Mint auswählen</string>
|
||||
<string name="cashu_memo_optional">Notiz (optional)</string>
|
||||
<string name="cashu_invoice_bolt11">Lightning-Rechnung (bolt11)</string>
|
||||
<string name="cashu_token_label">Cashu-Token</string>
|
||||
<string name="cashu_copy_invoice">Rechnung kopieren</string>
|
||||
<string name="cashu_request_invoice">Rechnung anfordern</string>
|
||||
<string name="cashu_discard_invoice">Rechnung verwerfen</string>
|
||||
<string name="cashu_history_incoming">Empfangen</string>
|
||||
<string name="cashu_history_outgoing">Gesendet</string>
|
||||
<string name="cashu_settings_title">Cashu-Wallet-Einstellungen</string>
|
||||
<string name="cashu_settings_edit_wallet">Wallet-Details bearbeiten</string>
|
||||
<string name="cashu_settings_edit_wallet_subtitle">Mints, Nutzap-Schlüssel</string>
|
||||
<string name="cashu_settings_my_recommendations">Meine Mint-Empfehlungen</string>
|
||||
<string name="cashu_settings_no_recommendations">Du hast noch keine Mints empfohlen. Tippe auf das Daumen-hoch-Symbol neben einem Mint, um ihn öffentlich zu empfehlen.</string>
|
||||
<string name="cashu_settings_delete_recommendation">Empfehlung löschen</string>
|
||||
<string name="cashu_settings_delete_confirm_title">Empfehlung zurückziehen?</string>
|
||||
<string name="cashu_settings_delete_confirm_body">Eine Löschanfrage für deine kind:38000-Empfehlung von %1$s veröffentlichen. Relays, die NIP-09 beachten, entfernen sie.</string>
|
||||
<string name="cashu_settings_add_recommendation">Einen Mint empfehlen</string>
|
||||
<string name="cashu_settings_restore_title">Aus Seed wiederherstellen</string>
|
||||
<string name="cashu_settings_restore_subtitle">Leite jedes frühere Geheimnis neu ab und bitte jeden Mint, alle noch gespeicherten blinden Signaturen zurückzugeben. Nützlich nach Geräteverlust oder dem Löschen deiner Token-Events durch ein bösartiges Relay.</string>
|
||||
<string name="cashu_settings_restore_running">Durchsuche Mints…</string>
|
||||
<string name="cashu_settings_restore_nothing_found">Keine wiederherstellbaren Proofs gefunden.</string>
|
||||
<string name="cashu_settings_restore_result">%1$s Sats über %2$s Proofs wiederhergestellt.</string>
|
||||
<string name="cashu_pay_invoice">Rechnung bezahlen</string>
|
||||
<string name="cashu_get_quote">Angebot einholen</string>
|
||||
<string name="cashu_getting_quote">Frage Mint nach einem Angebot…</string>
|
||||
<string name="cashu_quote_confirm">%1$s Sat + bis zu %2$s Sat Gebühren bezahlen?</string>
|
||||
<string name="cashu_verify">Prüfen</string>
|
||||
<string name="cashu_mint_reachable">✓ Mint ist erreichbar</string>
|
||||
<string name="cashu_mint_unreachable">Mint nicht erreichbar: %1$s</string>
|
||||
<string name="nutzap_failed_title">Nutzap fehlgeschlagen</string>
|
||||
<string name="nutzap_failed_no_recipient">Kein Empfänger-Pubkey in der Notiz</string>
|
||||
<string name="nutzap_failed_no_event">Ereignisreferenz kann nicht erstellt werden</string>
|
||||
<string name="cashu_create_token">Token erstellen</string>
|
||||
<string name="cashu_redeem_button">Einlösen</string>
|
||||
<string name="cashu_done">Fertig</string>
|
||||
<string name="cashu_requesting_invoice">Frage Mint nach einer Rechnung…</string>
|
||||
<string name="cashu_waiting_for_payment">Warte auf die Bezahlung der Rechnung…</string>
|
||||
<string name="cashu_completing_mint">Stelle Proofs aus…</string>
|
||||
<string name="cashu_paying_invoice">Zahle über Mint…</string>
|
||||
<string name="cashu_building_token">Tausche Proofs…</string>
|
||||
<string name="cashu_redeeming">Tausche Proofs in deine Wallet…</string>
|
||||
<string name="cashu_receive_invoice_explainer">Bezahle diese Rechnung, um %1$s Sats Ecash zu prägen.</string>
|
||||
<string name="cashu_received_amount">%1$s Sats empfangen.</string>
|
||||
<string name="cashu_paid_amount">%1$s Sats bezahlt (Gebühren: %2$s Sats).</string>
|
||||
<string name="cashu_token_ready">Token über %1$s Sats bereit. Teile ihn mit dem Empfänger.</string>
|
||||
<string name="cashu_redeemed_amount">%1$s Sats in deine Wallet eingelöst.</string>
|
||||
<string name="wallet_paste_uri">nostr+walletconnect://-URI einfügen</string>
|
||||
<string name="wallet_save">Speichern</string>
|
||||
<string name="wallet_rename">Umbenennen</string>
|
||||
@@ -1724,7 +1848,6 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="wallet_invalid_uri">Ungültige NWC-Verbindungs-URI</string>
|
||||
<string name="wallet_move_up">Nach oben</string>
|
||||
<string name="wallet_move_down">Nach unten</string>
|
||||
<string name="wallet_reorder">Wallets neu anordnen</string>
|
||||
<string name="wallet_onchain_transactions">On-Chain-Transaktionen</string>
|
||||
<string name="wallet_onchain_no_address">Keine On-Chain-Adresse für dieses Konto verfügbar.</string>
|
||||
<string name="wallet_onchain_no_backend">Kein On-Chain-Backend konfiguriert.</string>
|
||||
@@ -2317,6 +2440,10 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="kind_audio_track">Audiospur</string>
|
||||
<string name="kind_music_track">Musiktitel</string>
|
||||
<string name="kind_music_playlist">Musik-Playlist</string>
|
||||
<string name="kind_podcast_episode">Podcast-Folge</string>
|
||||
<string name="kind_podcast_metadata">Podcast-Sendung</string>
|
||||
<string name="kind_authored_podcasts">Erstellte Podcasts</string>
|
||||
<string name="kind_favorite_podcasts">Lieblings-Podcasts</string>
|
||||
<string name="kind_badge_awards">Abzeichen-Auszeichnungen</string>
|
||||
<string name="kind_badge_definitions">Abzeichen-Definitionen</string>
|
||||
<string name="kind_accepted_badge_set">Akzeptiertes Abzeichen-Set</string>
|
||||
@@ -2559,6 +2686,16 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<item quantity="one">%1$d Jahr</item>
|
||||
<item quantity="other">%1$d Jahre</item>
|
||||
</plurals>
|
||||
<plurals name="cashu_wallet_subtitle_mints">
|
||||
<item quantity="one">%1$d Mint</item>
|
||||
<item quantity="other">%1$d Mints</item>
|
||||
</plurals>
|
||||
<plurals name="cashu_pending_quotes_title">
|
||||
<item quantity="one">%1$d ausstehende Rechnung</item>
|
||||
<item quantity="other">%1$d ausstehende Rechnungen</item>
|
||||
</plurals>
|
||||
<string name="cashu_pending_quotes_subtitle">Tippe, um fortzusetzen und den Status zu prüfen.</string>
|
||||
<string name="cashu_pending_quotes_resume">Fortsetzen</string>
|
||||
<string name="event_sync_less_than_until"><%1$s</string>
|
||||
<string name="event_sync_status_connecting">Verbinden</string>
|
||||
<string name="event_sync_status_downloading">Herunterladen</string>
|
||||
|
||||
@@ -1824,7 +1824,6 @@
|
||||
<string name="wallet_invalid_uri">अमान्य नोस्टर धनकोष संयोजन पता</string>
|
||||
<string name="wallet_move_up">ऊर्ध्वगमन</string>
|
||||
<string name="wallet_move_down">अधोगमन</string>
|
||||
<string name="wallet_reorder">धनकोष पुनःव्यवस्थित करें</string>
|
||||
<string name="wallet_onchain_transactions">खण्डश्रृंखला सत्यापित लेनदेन</string>
|
||||
<string name="wallet_onchain_no_address">इस लेखा के लिए कोई खण्डश्रृंखला पता उपलब्ध नहीं।</string>
|
||||
<string name="wallet_onchain_no_backend">कोई खणडश्रृंखला पृष्ठभाग समाकृत नहीं।</string>
|
||||
|
||||
@@ -1824,7 +1824,6 @@
|
||||
<string name="wallet_invalid_uri">Érvénytelen NWC-kapcsolati cím</string>
|
||||
<string name="wallet_move_up">Mozgatás felfelé</string>
|
||||
<string name="wallet_move_down">Mozgatás lefelé</string>
|
||||
<string name="wallet_reorder">Pénztárca újrarendezése</string>
|
||||
<string name="wallet_onchain_transactions">Láncon belüli tranzakciók</string>
|
||||
<string name="wallet_onchain_no_address">Ehhez a fiókhoz nincs elérhető láncon belüli cím.</string>
|
||||
<string name="wallet_onchain_no_backend">Nincs beállítva lánc-háttérprogram.</string>
|
||||
|
||||
@@ -792,6 +792,12 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
|
||||
<string name="add_to_public_bookmarks">Dodaj do publicznych zakładek</string>
|
||||
<string name="remove_from_private_bookmarks">Usuń z prywatnych zakładek</string>
|
||||
<string name="remove_from_public_bookmarks">Usuń z publicznych zakładek</string>
|
||||
<string name="add_hashtag_label">Dodaj hasztag</string>
|
||||
<string name="add_hashtag_label_title">Dodaj hasztag</string>
|
||||
<string name="add_hashtag_label_explainer">Publicznie oznacz ten post hasztagiem (etykietą NIP-32). Osoby, które Cię obserwują, zobaczą go w kanale tego hasztagu.</string>
|
||||
<string name="add_hashtag_label_field">Hasztag</string>
|
||||
<string name="add_hashtag_label_confirm">Dodaj</string>
|
||||
<string name="hashtag_label_added_by">dodany przez</string>
|
||||
<string name="pinned_notes">Przypięte notatki</string>
|
||||
<string name="pinned_notes_explainer">Twoje przypięte notatki</string>
|
||||
<string name="pin_to_profile">Przypnij do profilu</string>
|
||||
@@ -1436,7 +1442,7 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
|
||||
<string name="geohash_explainer">Dodaje Geohash twojej lokalizacji do wpisu. Użytkownicy będą wiedzieli, że jesteś mniej niż 5 km od bieżącej lokalizacji</string>
|
||||
<string name="geohash_exclusive">Post ekskluzywny dla lokalizacji</string>
|
||||
<string name="geohash_exclusive_explainer">Tylko obserwatorzy z twojej lokalizacji to zobaczą. Twoi ogólni obserwatorzy nie zobaczą tego.</string>
|
||||
<string name="hashtag_exclusive">Wpis hashtagowy</string>
|
||||
<string name="hashtag_exclusive">Wpis hasztagowy</string>
|
||||
<string name="hashtag_exclusive_explainer">Tylko obserwujący ten hashtag to zobaczą. Twoi ogólni obserwujący nie zobaczą tego.</string>
|
||||
<string name="external_url_scope">Skomentuj na stronie internetowej</string>
|
||||
<string name="external_id_scope">Skomentuj na zewnętrznym serwisie</string>
|
||||
@@ -1869,7 +1875,6 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
|
||||
<string name="wallet_invalid_uri">Nieprawidłowy URI połączenia NWC</string>
|
||||
<string name="wallet_move_up">W górę</string>
|
||||
<string name="wallet_move_down">W dół</string>
|
||||
<string name="wallet_reorder">Powtórne zamów portfel</string>
|
||||
<string name="wallet_onchain_transactions">Transakcje w łańcuchu</string>
|
||||
<string name="wallet_onchain_no_address">Brak dostępnego adresu w łańcuchu dla tego konta.</string>
|
||||
<string name="wallet_onchain_no_backend">Nie skonfigurowano backendu łańcucha.</string>
|
||||
@@ -2332,7 +2337,7 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
|
||||
<string name="share_video">Udostępnij film…</string>
|
||||
<string name="unable_to_share_video">Nie można udostępnić filmu, spróbuj ponownie później…</string>
|
||||
<string name="downloading_video_for_sharing">Pobieranie filmu…</string>
|
||||
<string name="search_by_hashtag">Szukaj tagu: #%1$s</string>
|
||||
<string name="search_by_hashtag">Szukaj hasztagu: #%1$s</string>
|
||||
<string name="search_scope_all">Wszytko</string>
|
||||
<string name="search_scope_people">Osoby</string>
|
||||
<string name="search_scope_notes">Wpisy</string>
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
<item quantity="one">Esta publicação tem mais de %1$d hashtag</item>
|
||||
<item quantity="other">Esta publicação tem mais de %1$d hashtags</item>
|
||||
</plurals>
|
||||
<plurals name="nip82_assets_count">
|
||||
<item quantity="one">%1$d recurso incluído</item>
|
||||
<item quantity="other">%1$d recursos incluídos</item>
|
||||
</plurals>
|
||||
<string name="post_not_found">postagem não encontrada</string>
|
||||
<string name="post_not_found_short">👀</string>
|
||||
<string name="channel_image">Imagem do Canal</string>
|
||||
@@ -127,6 +131,7 @@
|
||||
<string name="write_a_message">Escreva uma mensagem…</string>
|
||||
<string name="post">Salvar</string>
|
||||
<string name="save">Salvar</string>
|
||||
<string name="send">Enviar</string>
|
||||
<string name="create">Criar</string>
|
||||
<string name="rename">Renomear</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
@@ -550,6 +555,16 @@
|
||||
<string name="profile_badges_description">Escolha quais dos selos que você recebeu aparecerão no seu perfil.</string>
|
||||
<string name="profile_badges_empty">Você ainda não recebeu nenhum selo.</string>
|
||||
<string name="pictures">Imagens</string>
|
||||
<string name="nip82_repository_label">Fonte: %1$s</string>
|
||||
<string name="nip82_download">Baixar</string>
|
||||
<string name="nip82_section_about">Sobre</string>
|
||||
<string name="nip82_section_platforms">Plataformas</string>
|
||||
<string name="nip82_section_topics">Tópicos</string>
|
||||
<string name="nip82_section_latest_release">Versão mais recente</string>
|
||||
<string name="nip82_section_comments">Comentários</string>
|
||||
<string name="nip82_no_comments">Seja o primeiro a comentar.</string>
|
||||
<string name="nip82_older_releases_show">Mostrar versões antigas</string>
|
||||
<string name="nip82_older_releases_hide">Ocultar versões antigas</string>
|
||||
<string name="calendars">Calendários</string>
|
||||
<string name="shorts">Curtas</string>
|
||||
<string name="public_chats">Chats públicos</string>
|
||||
@@ -710,12 +725,51 @@
|
||||
<string name="nests_servers_empty">Ainda não há servidores. Adicione um abaixo ou escolha um recomendado.</string>
|
||||
<string name="longs">Vídeos</string>
|
||||
<string name="articles">Artigos</string>
|
||||
<string name="route_music_tracks">Música</string>
|
||||
<string name="route_podcast_episodes">Episódios</string>
|
||||
<string name="podcast_view_episodes">Ver episódios</string>
|
||||
<string name="podcast_episodes_section">Episódios</string>
|
||||
<string name="podcast_no_episodes">Nenhum episódio encontrado ainda</string>
|
||||
<plurals name="podcast_episode_count">
|
||||
<item quantity="one">%1$d episódio</item>
|
||||
<item quantity="other">%1$d episódios</item>
|
||||
</plurals>
|
||||
<string name="new_music_track">Nova faixa de música</string>
|
||||
<string name="new_music_playlist">Nova playlist</string>
|
||||
<string name="music_track_title_label">Título</string>
|
||||
<string name="music_track_title_placeholder">Título da faixa</string>
|
||||
<string name="music_track_artist_label">Artista</string>
|
||||
<string name="music_track_artist_placeholder">Nome do artista</string>
|
||||
<string name="music_track_audio_url_label">URL do áudio</string>
|
||||
<string name="music_track_image_url_label">URL da imagem de capa (opcional)</string>
|
||||
<string name="music_track_album_label">Álbum (opcional)</string>
|
||||
<string name="music_track_duration_label">Duração em segundos (opcional)</string>
|
||||
<string name="music_track_description_label">Letra ou créditos (opcional)</string>
|
||||
<string name="music_track_delete">Excluir esta faixa</string>
|
||||
<string name="music_track_delete_confirm">Publicar uma exclusão NIP-09 para esta faixa de música?</string>
|
||||
<string name="add_to_music_playlist">Adicionar à playlist</string>
|
||||
<string name="add_to_music_playlist_title">Adicionar à playlist</string>
|
||||
<string name="add_to_music_playlist_empty">Você ainda não tem playlists. Use o campo acima para criar uma.</string>
|
||||
<string name="add_to_music_playlist_manage_all">Gerenciar todas as playlists</string>
|
||||
<string name="music_playlist_new_dialog_title">Nova playlist</string>
|
||||
<string name="music_playlist_new_title_placeholder">Nome da playlist</string>
|
||||
<string name="music_playlist_create_action">Criar</string>
|
||||
<string name="music_playlist_untitled">(playlist sem título)</string>
|
||||
<plurals name="music_playlist_track_count_short">
|
||||
<item quantity="one">%1$d faixa</item>
|
||||
<item quantity="other">%1$d faixas</item>
|
||||
</plurals>
|
||||
<string name="private_bookmarks">Itens Salvos Privados</string>
|
||||
<string name="public_bookmarks">Itens Salvos Públicos</string>
|
||||
<string name="add_to_private_bookmarks">Adicionar aos Itens Salvos Privados</string>
|
||||
<string name="add_to_public_bookmarks">Adicionar aos Itens Salvos Públicos</string>
|
||||
<string name="remove_from_private_bookmarks">Remover dos Itens Salvos Privados</string>
|
||||
<string name="remove_from_public_bookmarks">Remover dos Itens Salvos Públicos</string>
|
||||
<string name="add_hashtag_label">Adicionar hashtag</string>
|
||||
<string name="add_hashtag_label_title">Adicionar uma hashtag</string>
|
||||
<string name="add_hashtag_label_explainer">Marque publicamente este post com uma hashtag (etiqueta NIP-32). Quem segue você o verá no feed dessa hashtag.</string>
|
||||
<string name="add_hashtag_label_confirm">Adicionar</string>
|
||||
<string name="hashtag_label_added_by">adicionado por</string>
|
||||
<string name="pinned_notes">Notas Fixadas</string>
|
||||
<string name="pinned_notes_explainer">Suas notas fixadas</string>
|
||||
<string name="pin_to_profile">Fixar no Perfil</string>
|
||||
@@ -784,6 +838,28 @@
|
||||
<string name="quick_zap_amounts">Valores Rápidos de Zap</string>
|
||||
<string name="quick_zap_amounts_explainer">Exibido ao pressionar o botão de zap. Toque em um valor para removê-lo. Se deixar vazio, abrirá o diálogo para inserir um valor todas as vezes.</string>
|
||||
<string name="send_onchain_instead">Enviar on-chain em vez disso</string>
|
||||
<string name="reload_mint_title">Recarregar mint</string>
|
||||
<string name="reload_mint_topup_label">Quantia da recarga</string>
|
||||
<string name="reload_mint_section_to">Mint a recarregar</string>
|
||||
<string name="reload_mint_section_from">Fundos de</string>
|
||||
<string name="reload_mint_pay_lightning">Pagar com Lightning</string>
|
||||
<string name="reload_mint_lightning_desc">Emita novo ecash a partir da sua carteira Lightning</string>
|
||||
<string name="reload_mint_not_enough">Não há o suficiente aqui</string>
|
||||
<string name="reload_mint_available">%1$s sats disponíveis</string>
|
||||
<string name="reload_mint_confirm">Recarregar e enviar zap</string>
|
||||
<string name="reload_mint_send_confirm">Enviar zap</string>
|
||||
<string name="reload_mint_awaiting_payment">Aguardando pagamento…</string>
|
||||
<string name="reload_mint_retry">Tentar de novo</string>
|
||||
<string name="reload_mint_summary">Recarregue %1$s sats para %2$s e envie zap de %3$s %4$s sats · taxa ≈ %5$s sats</string>
|
||||
<string name="reload_mint_summary_funded">Zap de %1$s %2$s sats de %3$s</string>
|
||||
<string name="reload_mint_recipient_fallback">o destinatário</string>
|
||||
<string name="reload_mint_needs_more">faltam %1$s sat</string>
|
||||
<string name="reload_mint_funded">financiado</string>
|
||||
<string name="reload_mint_copy_invoice">Copiar fatura</string>
|
||||
<string name="topup_mint_title">Recarregar mint</string>
|
||||
<string name="topup_mint_action">Recarregar este mint</string>
|
||||
<string name="topup_mint_amount_label">Quantia a adicionar</string>
|
||||
<string name="topup_mint_confirm">Recarregar</string>
|
||||
<string name="zap_privacy_section">Privacidade do Zap</string>
|
||||
<string name="zap_type_section_explainer">Controla como sua identidade é exibida ao enviar um Zap.</string>
|
||||
<string name="wallet_connect_connect_app">Conectar Carteira</string>
|
||||
@@ -874,6 +950,7 @@
|
||||
<string name="payment_target_authority">Endereço</string>
|
||||
<string name="delete_payment_target">Excluir destino de pagamento</string>
|
||||
<string name="no_payment_app_found">Nenhum aplicativo encontrado para processar este pagamento. Por favor, instale uma carteira compatível.</string>
|
||||
<string name="no_payment_app_found_for_type">Nenhum app instalado para processar pagamentos %1$s. Instale uma carteira compatível.</string>
|
||||
<string name="error_dialog_payment_error">Não foi possível abrir o pagamento</string>
|
||||
<string name="uploading_state_ready">Não iniciado</string>
|
||||
<string name="uploading_state_compressing">Compactando</string>
|
||||
@@ -1182,6 +1259,8 @@
|
||||
<string name="posting_policy">Política de postagem</string>
|
||||
<string name="privacy_policy">Política de privacidade</string>
|
||||
<string name="terms_and_conditions">Termos & condições</string>
|
||||
<string name="child_safety_standards">Padrões de segurança infantil</string>
|
||||
<string name="about_legal">Sobre e Jurídico</string>
|
||||
<string name="not_available_acronym">N/A</string>
|
||||
<string name="relay_error_messages">Erros e Avisos deste Relé</string>
|
||||
<string name="relay_monitor_reports">Relatórios do Monitor de Relay</string>
|
||||
@@ -1341,6 +1420,26 @@
|
||||
<string name="external_url_scope">Comentar em um site</string>
|
||||
<string name="external_id_scope">Comentar em um recurso externo</string>
|
||||
<string name="long_form_reading_minutes">%1$d min de leitura</string>
|
||||
<plurals name="music_playlist_track_count">
|
||||
<item quantity="one">%1$d faixa</item>
|
||||
<item quantity="other">%1$d faixas</item>
|
||||
</plurals>
|
||||
<plurals name="music_playlist_more_tracks">
|
||||
<item quantity="one">+%1$d faixa a mais</item>
|
||||
<item quantity="other">+%1$d faixas a mais</item>
|
||||
</plurals>
|
||||
<string name="music_playlist_collaborative">Colaborativa</string>
|
||||
<string name="music_playlist_private">Privada</string>
|
||||
<string name="music_playlist_unknown_track">Faixa desconhecida</string>
|
||||
<string name="music_playlist_loading_track">Carregando faixa…</string>
|
||||
<string name="music_track_cover_upload_cta">Enviar uma imagem de capa</string>
|
||||
<string name="music_track_cover_upload_hint">Escolha uma imagem quadrada para ser a arte desta faixa.</string>
|
||||
<string name="music_track_audio_upload_cta">Enviar um arquivo de áudio</string>
|
||||
<string name="music_track_audio_upload_hint">Escolha um MP3, WAV ou FLAC. Ele será enviado ao seu servidor de mídia ao salvar.</string>
|
||||
<string name="music_track_audio_picked">Arquivo de áudio pronto para envio</string>
|
||||
<string name="music_track_uploading">Enviando…</string>
|
||||
<string name="music_track_uploading_banner">Enviando capa + áudio e publicando. Isso continuará mesmo se você sair da tela.</string>
|
||||
<string name="music_track_upload_failed">Falha no envio: %1$s</string>
|
||||
<string name="loading_location">Carregando localização</string>
|
||||
<string name="lack_location_permissions">Sem permissões para localização</string>
|
||||
<string name="add_sensitive_content_explainer">Adiciona aviso de conteúdo sensível antes de mostrar seu conteúdo. Isso é ideal para qualquer conteúdo NSFW ou conteúdo que algumas pessoas possam considerar ofensivo ou perturbador</string>
|
||||
@@ -1435,6 +1534,9 @@
|
||||
<string name="wallet_number">Carteira %1$s</string>
|
||||
<string name="error_opening_external_signer">Erro ao abrir o aplicativo para assinar evento</string>
|
||||
<string name="error_opening_external_signer_description">O aplicativo de assinatura não pôde ser encontrado. Verifique se o aplicativo não foi desinstalado</string>
|
||||
<string name="error_video_playback_failed">Não é possível reproduzir este vídeo</string>
|
||||
<string name="error_video_playback_failed_description">O codec ou formato não é compatível com este dispositivo (%1$s). Tente abri-lo no seu navegador.</string>
|
||||
<string name="error_video_open_in_browser">Abrir no navegador</string>
|
||||
<string name="sign_request_rejected2">Solicitação de assinatura rejeitada</string>
|
||||
<string name="sign_request_rejected">Solicitação de assinatura rejeitada</string>
|
||||
<string name="sign_request_rejected_description">Certifique-se de que a aplicação assinante autorizou esta transação</string>
|
||||
@@ -1583,6 +1685,7 @@
|
||||
<string name="metadata_strip_failed_upload">Enviar mesmo assim</string>
|
||||
<string name="metadata_strip_failed_upload_cancelled">Não foi possível remover metadados privados da mídia. Envio cancelado.</string>
|
||||
<string name="upload_cancelled">Envio cancelado</string>
|
||||
<string name="avif_metadata_strip_failed">Não foi possível remover os metadados do AVIF: %1$s</string>
|
||||
<string name="edit_draft">Editar rascunho</string>
|
||||
<string name="login_with_qr_code">Entrar com Código QR</string>
|
||||
<string name="route">Rota</string>
|
||||
@@ -1637,6 +1740,94 @@
|
||||
<string name="wallet_no_wallets">Nenhuma carteira conectada</string>
|
||||
<string name="wallet_no_wallets_description">Conecte uma ou mais carteiras NWC para enviar e receber pagamentos.</string>
|
||||
<string name="wallet_add_connection">Adicionar conexão NWC</string>
|
||||
<string name="wallet_add_choose_type">Escolha o tipo de carteira</string>
|
||||
<string name="wallet_add_nwc_title">Conectar uma carteira Lightning (NWC)</string>
|
||||
<string name="wallet_add_nwc_description">Pague por uma carteira Lightning externa usando o Nostr Wallet Connect.</string>
|
||||
<string name="wallet_add_cashu_title">Carteira Cashu (NIP-60)</string>
|
||||
<string name="wallet_add_cashu_description">Uma carteira ecash dentro do app, baseada em um mint Cashu. Os tokens são armazenados criptografados em relays.</string>
|
||||
<string name="cashu_wallet_title">Carteira Cashu</string>
|
||||
<string name="cashu_no_wallet">Ainda sem carteira Cashu</string>
|
||||
<string name="cashu_no_wallet_description">Crie uma carteira Cashu para guardar tokens ecash e receber nutzaps.</string>
|
||||
<string name="cashu_discovering">Procurando sua carteira…</string>
|
||||
<string name="cashu_discovering_description">Carteiras NIP-60 sincronizam entre clientes. Se você criou uma em outro app com esta chave Nostr, ela deve aparecer em alguns segundos.</string>
|
||||
<string name="cashu_balance">Saldo</string>
|
||||
<string name="cashu_mint_url">URL do mint</string>
|
||||
<string name="cashu_remove_mint">Remover mint</string>
|
||||
<string name="cashu_history">Histórico</string>
|
||||
<string name="cashu_edit_wallet">Editar carteira</string>
|
||||
<string name="cashu_p2pk_section">Chave de nutzap (avançado)</string>
|
||||
<string name="cashu_p2pk_explainer">Uma chave privada separada, usada apenas para receber nutzaps NIP-61. Não é a chave da sua identidade Nostr.</string>
|
||||
<string name="cashu_p2pk_autogen">Gerar uma nova chave</string>
|
||||
<string name="cashu_p2pk_autogen_warning_edit">Invalidará quaisquer nutzaps pendentes vinculados à chave atual.</string>
|
||||
<string name="cashu_p2pk_keep_current">Manter chave atual</string>
|
||||
<string name="cashu_p2pk_manual_label">Chave privada P2PK (hex)</string>
|
||||
<string name="wallet_edit_cashu_title">Editar carteira Cashu</string>
|
||||
<string name="wallet_save_changes">Salvar alterações</string>
|
||||
<string name="cashu_mint_no_recs">Ainda sem recomendações</string>
|
||||
<string name="cashu_mint_recommended_by">Recomendado por</string>
|
||||
<plurals name="cashu_mint_plus_others">
|
||||
<item quantity="one">+%1$d mais</item>
|
||||
<item quantity="other">+%1$d mais</item>
|
||||
</plurals>
|
||||
<plurals name="cashu_mint_recommended_by_others">
|
||||
<item quantity="one">Recomendado por %1$d outro</item>
|
||||
<item quantity="other">Recomendado por %1$d outros</item>
|
||||
</plurals>
|
||||
<string name="cashu_mint_popular_header">Mints populares</string>
|
||||
<string name="cashu_mint_matches_header">Mints correspondentes</string>
|
||||
<string name="cashu_recommend_mint">Recomendar este mint</string>
|
||||
<string name="cashu_action_receive">Receber</string>
|
||||
<string name="cashu_action_send_ln">Enviar LN</string>
|
||||
<string name="cashu_action_send_token">Enviar token</string>
|
||||
<string name="cashu_action_redeem">Resgatar</string>
|
||||
<string name="cashu_amount_sats">Quantia (sats)</string>
|
||||
<string name="cashu_pick_mint">Escolha um mint</string>
|
||||
<string name="cashu_memo_optional">Memo (opcional)</string>
|
||||
<string name="cashu_invoice_bolt11">Fatura Lightning (bolt11)</string>
|
||||
<string name="cashu_token_label">Token Cashu</string>
|
||||
<string name="cashu_copy_invoice">Copiar fatura</string>
|
||||
<string name="cashu_request_invoice">Solicitar fatura</string>
|
||||
<string name="cashu_discard_invoice">Descartar fatura</string>
|
||||
<string name="cashu_history_incoming">Recebido</string>
|
||||
<string name="cashu_history_outgoing">Enviado</string>
|
||||
<string name="cashu_settings_title">Configurações da carteira Cashu</string>
|
||||
<string name="cashu_settings_edit_wallet">Editar detalhes da carteira</string>
|
||||
<string name="cashu_settings_edit_wallet_subtitle">Mints, chave de nutzap</string>
|
||||
<string name="cashu_settings_my_recommendations">Minhas recomendações de mint</string>
|
||||
<string name="cashu_settings_no_recommendations">Você ainda não recomendou nenhum mint. Toque no joinha ao lado de um mint para recomendá-lo publicamente.</string>
|
||||
<string name="cashu_settings_delete_recommendation">Excluir recomendação</string>
|
||||
<string name="cashu_settings_delete_confirm_title">Retirar recomendação?</string>
|
||||
<string name="cashu_settings_delete_confirm_body">Publicar um pedido de exclusão para sua recomendação kind:38000 de %1$s. Relays que respeitam o NIP-09 irão removê-la.</string>
|
||||
<string name="cashu_settings_add_recommendation">Recomendar um mint</string>
|
||||
<string name="cashu_settings_restore_title">Recuperar a partir da seed</string>
|
||||
<string name="cashu_settings_restore_subtitle">Rederive cada segredo antigo e peça a cada mint que retorne quaisquer assinaturas cegas ainda registradas. Útil após a perda de um dispositivo ou a exclusão dos seus eventos de token por um relay malicioso.</string>
|
||||
<string name="cashu_settings_restore_running">Escaneando mints…</string>
|
||||
<string name="cashu_settings_restore_nothing_found">Nenhuma prova recuperável encontrada.</string>
|
||||
<string name="cashu_settings_restore_result">Recuperados %1$s sats em %2$s provas.</string>
|
||||
<string name="cashu_pay_invoice">Pagar fatura</string>
|
||||
<string name="cashu_get_quote">Obter cotação</string>
|
||||
<string name="cashu_getting_quote">Pedindo cotação ao mint…</string>
|
||||
<string name="cashu_quote_confirm">Pagar %1$s sat + até %2$s sat em taxas?</string>
|
||||
<string name="cashu_verify">Verificar</string>
|
||||
<string name="cashu_mint_reachable">✓ O mint está acessível</string>
|
||||
<string name="cashu_mint_unreachable">Não foi possível acessar o mint: %1$s</string>
|
||||
<string name="nutzap_failed_title">Falha no nutzap</string>
|
||||
<string name="nutzap_failed_no_recipient">Sem chave pública do destinatário na nota</string>
|
||||
<string name="nutzap_failed_no_event">Não foi possível construir a referência do evento</string>
|
||||
<string name="cashu_create_token">Criar token</string>
|
||||
<string name="cashu_redeem_button">Resgatar</string>
|
||||
<string name="cashu_done">Concluído</string>
|
||||
<string name="cashu_requesting_invoice">Pedindo uma fatura ao mint…</string>
|
||||
<string name="cashu_waiting_for_payment">Aguardando o pagamento da fatura…</string>
|
||||
<string name="cashu_completing_mint">Emitindo provas…</string>
|
||||
<string name="cashu_paying_invoice">Pagando via mint…</string>
|
||||
<string name="cashu_building_token">Trocando provas…</string>
|
||||
<string name="cashu_redeeming">Trocando provas para sua carteira…</string>
|
||||
<string name="cashu_receive_invoice_explainer">Pague esta fatura para emitir %1$s sats de ecash.</string>
|
||||
<string name="cashu_received_amount">Recebido %1$s sats.</string>
|
||||
<string name="cashu_paid_amount">Pago %1$s sats (taxas: %2$s sats).</string>
|
||||
<string name="cashu_token_ready">Token de %1$s sats pronto. Compartilhe com o destinatário.</string>
|
||||
<string name="cashu_redeemed_amount">Resgatado %1$s sats para sua carteira.</string>
|
||||
<string name="wallet_paste_uri">Colar URI nostr+walletconnect://</string>
|
||||
<string name="wallet_save">Salvar</string>
|
||||
<string name="wallet_rename">Renomear</string>
|
||||
@@ -1738,6 +1929,7 @@
|
||||
<string name="calendar_rsvp_going">Vou</string>
|
||||
<string name="calendar_rsvp_maybe">Talvez</string>
|
||||
<string name="calendar_rsvp_not_going">Não vou</string>
|
||||
<string name="calendar_rsvp_section">Confirmações (%1$d)</string>
|
||||
<string name="calendar_rsvp_none">Sem RSVPs ainda.</string>
|
||||
<string name="calendar_participants_section">Participantes (%1$d)</string>
|
||||
<string name="calendar_event_in_calendars">Em calendários (%1$d)</string>
|
||||
@@ -2226,6 +2418,12 @@
|
||||
<string name="kind_user_settings">Configurações do Usuário</string>
|
||||
<string name="kind_audio_header">Cabeçalho de Áudio</string>
|
||||
<string name="kind_audio_track">Faixa de Áudio</string>
|
||||
<string name="kind_music_track">Faixa de música</string>
|
||||
<string name="kind_music_playlist">Playlist de música</string>
|
||||
<string name="kind_podcast_episode">Episódio de podcast</string>
|
||||
<string name="kind_podcast_metadata">Programa de podcast</string>
|
||||
<string name="kind_authored_podcasts">Podcasts criados</string>
|
||||
<string name="kind_favorite_podcasts">Podcasts favoritos</string>
|
||||
<string name="kind_badge_awards">Concessões de Emblemas</string>
|
||||
<string name="kind_badge_definitions">Definições de Emblemas</string>
|
||||
<string name="kind_accepted_badge_set">Conjunto de Distintivos Aceito</string>
|
||||
@@ -2468,6 +2666,12 @@
|
||||
<item quantity="one">%1$d ano</item>
|
||||
<item quantity="other">%1$d anos</item>
|
||||
</plurals>
|
||||
<plurals name="cashu_pending_quotes_title">
|
||||
<item quantity="one">%1$d fatura pendente</item>
|
||||
<item quantity="other">%1$d faturas pendentes</item>
|
||||
</plurals>
|
||||
<string name="cashu_pending_quotes_subtitle">Toque para retomar e verificar o status.</string>
|
||||
<string name="cashu_pending_quotes_resume">Retomar</string>
|
||||
<string name="event_sync_less_than_until"><%1$s</string>
|
||||
<string name="event_sync_status_connecting">Conectando</string>
|
||||
<string name="event_sync_status_downloading">Baixando</string>
|
||||
|
||||
@@ -731,6 +731,15 @@
|
||||
<string name="articles">Artiklar</string>
|
||||
<string name="route_music_tracks">Musik</string>
|
||||
<string name="route_music_playlists">Spellistor</string>
|
||||
<string name="route_podcast_episodes">Avsnitt</string>
|
||||
<string name="route_podcasts">Poddar</string>
|
||||
<string name="podcast_view_episodes">Visa avsnitt</string>
|
||||
<string name="podcast_episodes_section">Avsnitt</string>
|
||||
<string name="podcast_no_episodes">Inga avsnitt hittades än</string>
|
||||
<plurals name="podcast_episode_count">
|
||||
<item quantity="one">%1$d avsnitt</item>
|
||||
<item quantity="other">%1$d avsnitt</item>
|
||||
</plurals>
|
||||
<string name="new_music_track">Nytt musikspår</string>
|
||||
<string name="new_music_playlist">Ny spellista</string>
|
||||
<string name="music_track_title_label">Titel</string>
|
||||
@@ -763,6 +772,11 @@
|
||||
<string name="add_to_public_bookmarks">Lägg till i Publika Bokmärken</string>
|
||||
<string name="remove_from_private_bookmarks">Ta bort från Privata Bokmärken</string>
|
||||
<string name="remove_from_public_bookmarks">Ta bort från Publika Bokmärken</string>
|
||||
<string name="add_hashtag_label">Lägg till hashtag</string>
|
||||
<string name="add_hashtag_label_title">Lägg till en hashtag</string>
|
||||
<string name="add_hashtag_label_explainer">Tagga det här inlägget offentligt med en hashtag (NIP-32-etikett). De som följer dig ser det i den hashtaggens flöde.</string>
|
||||
<string name="add_hashtag_label_confirm">Lägg till</string>
|
||||
<string name="hashtag_label_added_by">tillagd av</string>
|
||||
<string name="pinned_notes">Fästa anteckningar</string>
|
||||
<string name="pinned_notes_explainer">Dina fastnålade anteckningar</string>
|
||||
<string name="pin_to_profile">Fäst på profil</string>
|
||||
@@ -831,6 +845,28 @@
|
||||
<string name="quick_zap_amounts">Snabba Zap-belopp</string>
|
||||
<string name="quick_zap_amounts_explainer">Visas när du trycker på Zap-knappen. Tryck på ett belopp för att ta bort det. Om du lämnar det tomt öppnas en dialog för att ange ett belopp varje gång.</string>
|
||||
<string name="send_onchain_instead">Skicka on-chain istället</string>
|
||||
<string name="reload_mint_title">Fyll på mint</string>
|
||||
<string name="reload_mint_topup_label">Påfyllningsbelopp</string>
|
||||
<string name="reload_mint_section_to">Mint att fylla på</string>
|
||||
<string name="reload_mint_section_from">Medel från</string>
|
||||
<string name="reload_mint_pay_lightning">Betala med Lightning</string>
|
||||
<string name="reload_mint_lightning_desc">Utfärda nytt ecash från din Lightning-plånbok</string>
|
||||
<string name="reload_mint_not_enough">Inte tillräckligt här</string>
|
||||
<string name="reload_mint_available">%1$s sats tillgängliga</string>
|
||||
<string name="reload_mint_confirm">Fyll på & skicka zap</string>
|
||||
<string name="reload_mint_send_confirm">Skicka zap</string>
|
||||
<string name="reload_mint_awaiting_payment">Väntar på betalning…</string>
|
||||
<string name="reload_mint_retry">Försök igen</string>
|
||||
<string name="reload_mint_summary">Fyll på %1$s sats till %2$s och zappa %3$s %4$s sats · avgift ≈ %5$s sats</string>
|
||||
<string name="reload_mint_summary_funded">Zappa %1$s %2$s sats från %3$s</string>
|
||||
<string name="reload_mint_recipient_fallback">mottagaren</string>
|
||||
<string name="reload_mint_needs_more">behöver %1$s sat till</string>
|
||||
<string name="reload_mint_funded">finansierad</string>
|
||||
<string name="reload_mint_copy_invoice">Kopiera faktura</string>
|
||||
<string name="topup_mint_title">Fyll på mint</string>
|
||||
<string name="topup_mint_action">Fyll på den här mint</string>
|
||||
<string name="topup_mint_amount_label">Belopp att lägga till</string>
|
||||
<string name="topup_mint_confirm">Fyll på</string>
|
||||
<string name="zap_privacy_section">Zap-sekretess</string>
|
||||
<string name="zap_type_section_explainer">Styr hur din identitet visas när du skickar ett Zap.</string>
|
||||
<string name="wallet_connect_connect_app">Anslut plånbok</string>
|
||||
@@ -1710,6 +1746,94 @@
|
||||
<string name="wallet_no_wallets">Inga plånböcker anslutna</string>
|
||||
<string name="wallet_no_wallets_description">Anslut en eller flera NWC-plånböcker för att skicka och ta emot betalningar.</string>
|
||||
<string name="wallet_add_connection">Lägg till NWC-anslutning</string>
|
||||
<string name="wallet_add_choose_type">Välj plånbokstyp</string>
|
||||
<string name="wallet_add_nwc_title">Anslut en Lightning-plånbok (NWC)</string>
|
||||
<string name="wallet_add_nwc_description">Betala via en extern Lightning-plånbok med Nostr Wallet Connect.</string>
|
||||
<string name="wallet_add_cashu_title">Cashu-plånbok (NIP-60)</string>
|
||||
<string name="wallet_add_cashu_description">En ecash-plånbok i appen som backas av en Cashu-mint. Tokens lagras krypterade på relayer.</string>
|
||||
<string name="cashu_wallet_title">Cashu-plånbok</string>
|
||||
<string name="cashu_no_wallet">Ingen Cashu-plånbok än</string>
|
||||
<string name="cashu_no_wallet_description">Skapa en Cashu-plånbok för att hålla ecash-tokens och ta emot nutzaps.</string>
|
||||
<string name="cashu_discovering">Letar efter din plånbok…</string>
|
||||
<string name="cashu_discovering_description">NIP-60-plånböcker synkroniseras mellan klienter. Om du skapade en i en annan app med den här Nostr-nyckeln bör den dyka upp inom några sekunder.</string>
|
||||
<string name="cashu_balance">Saldo</string>
|
||||
<string name="cashu_mint_url">Mint-URL</string>
|
||||
<string name="cashu_remove_mint">Ta bort mint</string>
|
||||
<string name="cashu_history">Historik</string>
|
||||
<string name="cashu_edit_wallet">Redigera plånbok</string>
|
||||
<string name="cashu_p2pk_section">Nutzap-nyckel (avancerat)</string>
|
||||
<string name="cashu_p2pk_explainer">En separat privat nyckel som endast används för att ta emot NIP-61-nutzaps. Inte din Nostr-identitetsnyckel.</string>
|
||||
<string name="cashu_p2pk_autogen">Generera en ny nyckel</string>
|
||||
<string name="cashu_p2pk_autogen_warning_edit">Ogiltigförklarar alla väntande nutzaps som är låsta till den nuvarande nyckeln.</string>
|
||||
<string name="cashu_p2pk_keep_current">Behåll nuvarande nyckel</string>
|
||||
<string name="cashu_p2pk_manual_label">Privat P2PK-nyckel (hex)</string>
|
||||
<string name="wallet_edit_cashu_title">Redigera Cashu-plånbok</string>
|
||||
<string name="wallet_save_changes">Spara ändringar</string>
|
||||
<string name="cashu_mint_no_recs">Inga rekommendationer än</string>
|
||||
<string name="cashu_mint_recommended_by">Rekommenderad av</string>
|
||||
<plurals name="cashu_mint_plus_others">
|
||||
<item quantity="one">+%1$d till</item>
|
||||
<item quantity="other">+%1$d till</item>
|
||||
</plurals>
|
||||
<plurals name="cashu_mint_recommended_by_others">
|
||||
<item quantity="one">Rekommenderad av %1$d annan</item>
|
||||
<item quantity="other">Rekommenderad av %1$d andra</item>
|
||||
</plurals>
|
||||
<string name="cashu_mint_popular_header">Populära mints</string>
|
||||
<string name="cashu_mint_matches_header">Matchande mints</string>
|
||||
<string name="cashu_recommend_mint">Rekommendera den här mint</string>
|
||||
<string name="cashu_action_receive">Ta emot</string>
|
||||
<string name="cashu_action_send_ln">Skicka LN</string>
|
||||
<string name="cashu_action_send_token">Skicka token</string>
|
||||
<string name="cashu_action_redeem">Lös in</string>
|
||||
<string name="cashu_amount_sats">Belopp (sats)</string>
|
||||
<string name="cashu_pick_mint">Välj en mint</string>
|
||||
<string name="cashu_memo_optional">Notering (valfritt)</string>
|
||||
<string name="cashu_invoice_bolt11">Lightning-faktura (bolt11)</string>
|
||||
<string name="cashu_token_label">Cashu-token</string>
|
||||
<string name="cashu_copy_invoice">Kopiera faktura</string>
|
||||
<string name="cashu_request_invoice">Begär faktura</string>
|
||||
<string name="cashu_discard_invoice">Förkasta faktura</string>
|
||||
<string name="cashu_history_incoming">Mottaget</string>
|
||||
<string name="cashu_history_outgoing">Skickat</string>
|
||||
<string name="cashu_settings_title">Inställningar för Cashu-plånbok</string>
|
||||
<string name="cashu_settings_edit_wallet">Redigera plånboksuppgifter</string>
|
||||
<string name="cashu_settings_edit_wallet_subtitle">Mints, nutzap-nyckel</string>
|
||||
<string name="cashu_settings_my_recommendations">Mina mint-rekommendationer</string>
|
||||
<string name="cashu_settings_no_recommendations">Du har inte rekommenderat några mints än. Tryck på tummen upp bredvid en mint för att rekommendera den offentligt.</string>
|
||||
<string name="cashu_settings_delete_recommendation">Radera rekommendation</string>
|
||||
<string name="cashu_settings_delete_confirm_title">Dra tillbaka rekommendation?</string>
|
||||
<string name="cashu_settings_delete_confirm_body">Publicera en raderingsbegäran för din kind:38000-rekommendation av %1$s. Relayer som följer NIP-09 tar bort den.</string>
|
||||
<string name="cashu_settings_add_recommendation">Rekommendera en mint</string>
|
||||
<string name="cashu_settings_restore_title">Återställ från seed</string>
|
||||
<string name="cashu_settings_restore_subtitle">Härled varje tidigare hemlighet på nytt och be varje mint att returnera alla blinda signaturer som fortfarande finns sparade. Användbart efter en förlorad enhet eller en oseriös relays radering av dina token-händelser.</string>
|
||||
<string name="cashu_settings_restore_running">Skannar mints…</string>
|
||||
<string name="cashu_settings_restore_nothing_found">Inga återvinningsbara proofs hittades.</string>
|
||||
<string name="cashu_settings_restore_result">Återställde %1$s sats över %2$s proofs.</string>
|
||||
<string name="cashu_pay_invoice">Betala faktura</string>
|
||||
<string name="cashu_get_quote">Hämta offert</string>
|
||||
<string name="cashu_getting_quote">Ber mint om en offert…</string>
|
||||
<string name="cashu_quote_confirm">Betala %1$s sat + upp till %2$s sat i avgifter?</string>
|
||||
<string name="cashu_verify">Verifiera</string>
|
||||
<string name="cashu_mint_reachable">✓ Mint är nåbar</string>
|
||||
<string name="cashu_mint_unreachable">Kunde inte nå mint: %1$s</string>
|
||||
<string name="nutzap_failed_title">Nutzap misslyckades</string>
|
||||
<string name="nutzap_failed_no_recipient">Ingen mottagar-pubkey på noten</string>
|
||||
<string name="nutzap_failed_no_event">Kan inte bygga händelsereferens</string>
|
||||
<string name="cashu_create_token">Skapa token</string>
|
||||
<string name="cashu_redeem_button">Lös in</string>
|
||||
<string name="cashu_done">Klar</string>
|
||||
<string name="cashu_requesting_invoice">Ber mint om en faktura…</string>
|
||||
<string name="cashu_waiting_for_payment">Väntar på att fakturan ska betalas…</string>
|
||||
<string name="cashu_completing_mint">Utfärdar proofs…</string>
|
||||
<string name="cashu_paying_invoice">Betalar via mint…</string>
|
||||
<string name="cashu_building_token">Byter proofs…</string>
|
||||
<string name="cashu_redeeming">Byter proofs till din plånbok…</string>
|
||||
<string name="cashu_receive_invoice_explainer">Betala den här fakturan för att utfärda %1$s sats ecash.</string>
|
||||
<string name="cashu_received_amount">Tog emot %1$s sats.</string>
|
||||
<string name="cashu_paid_amount">Betalade %1$s sats (avgifter: %2$s sats).</string>
|
||||
<string name="cashu_token_ready">Token för %1$s sats redo. Dela den med mottagaren.</string>
|
||||
<string name="cashu_redeemed_amount">Löste in %1$s sats till din plånbok.</string>
|
||||
<string name="wallet_paste_uri">Klistra in nostr+walletconnect://-URI</string>
|
||||
<string name="wallet_save">Spara</string>
|
||||
<string name="wallet_rename">Byt namn</string>
|
||||
@@ -1718,7 +1842,6 @@
|
||||
<string name="wallet_invalid_uri">Ogiltig URI för NWC-anslutning</string>
|
||||
<string name="wallet_move_up">Flytta upp</string>
|
||||
<string name="wallet_move_down">Flytta ner</string>
|
||||
<string name="wallet_reorder">Ändra ordning på plånböcker</string>
|
||||
<string name="wallet_onchain_transactions">On-chain-transaktioner</string>
|
||||
<string name="wallet_onchain_no_address">Ingen on-chain-adress tillgänglig för detta konto.</string>
|
||||
<string name="wallet_onchain_no_backend">Ingen on-chain-backend är konfigurerad.</string>
|
||||
@@ -2311,6 +2434,10 @@
|
||||
<string name="kind_audio_track">Ljudspår</string>
|
||||
<string name="kind_music_track">Musikspår</string>
|
||||
<string name="kind_music_playlist">Musikspellista</string>
|
||||
<string name="kind_podcast_episode">Poddavsnitt</string>
|
||||
<string name="kind_podcast_metadata">Poddprogram</string>
|
||||
<string name="kind_authored_podcasts">Skapade poddar</string>
|
||||
<string name="kind_favorite_podcasts">Favoritpoddar</string>
|
||||
<string name="kind_badge_awards">Utmärkelser</string>
|
||||
<string name="kind_badge_definitions">Utmärkelsedefinitioner</string>
|
||||
<string name="kind_accepted_badge_set">Accepterad emblemuppsättning</string>
|
||||
@@ -2553,6 +2680,12 @@
|
||||
<item quantity="one">%1$d år</item>
|
||||
<item quantity="other">%1$d år</item>
|
||||
</plurals>
|
||||
<plurals name="cashu_pending_quotes_title">
|
||||
<item quantity="one">%1$d väntande faktura</item>
|
||||
<item quantity="other">%1$d väntande fakturor</item>
|
||||
</plurals>
|
||||
<string name="cashu_pending_quotes_subtitle">Tryck för att återuppta och kontrollera status.</string>
|
||||
<string name="cashu_pending_quotes_resume">Återuppta</string>
|
||||
<string name="event_sync_less_than_until"><%1$s</string>
|
||||
<string name="event_sync_status_connecting">Ansluter</string>
|
||||
<string name="event_sync_status_downloading">Laddar ner</string>
|
||||
|
||||
@@ -764,6 +764,12 @@
|
||||
<string name="add_to_public_bookmarks">添加到公开书签</string>
|
||||
<string name="remove_from_private_bookmarks">从私人书签中移除</string>
|
||||
<string name="remove_from_public_bookmarks">从公开书签中移除</string>
|
||||
<string name="add_hashtag_label">添加话题标签</string>
|
||||
<string name="add_hashtag_label_title">添加话题标签</string>
|
||||
<string name="add_hashtag_label_explainer">给这篇帖子加公开话题标签 (NIP-32 标签) 。关注你的人会在话题的源里看到。</string>
|
||||
<string name="add_hashtag_label_field">话题标签</string>
|
||||
<string name="add_hashtag_label_confirm">添加</string>
|
||||
<string name="hashtag_label_added_by">添加者</string>
|
||||
<string name="pinned_notes">置顶的笔记</string>
|
||||
<string name="pinned_notes_explainer">您置顶的笔记</string>
|
||||
<string name="pin_to_profile">置顶到个人资料</string>
|
||||
@@ -832,6 +838,29 @@
|
||||
<string name="quick_zap_amounts">快速打闪金额</string>
|
||||
<string name="quick_zap_amounts_explainer">按下打闪按钮时显示。点击接即可删除它。 如果留空,每次都会打开对话框来插入金融。</string>
|
||||
<string name="send_onchain_instead">在链上发送</string>
|
||||
<string name="reload_mint_title">铸币厂充值</string>
|
||||
<string name="reload_mint_sats_amount">%1$s sats</string>
|
||||
<string name="reload_mint_topup_label">充值金额</string>
|
||||
<string name="reload_mint_section_to">要充值的铸币厂</string>
|
||||
<string name="reload_mint_section_from">资金来源</string>
|
||||
<string name="reload_mint_pay_lightning">用闪电付款</string>
|
||||
<string name="reload_mint_lightning_desc">从闪电钱包铸造新的电子现金</string>
|
||||
<string name="reload_mint_not_enough">这里还不够</string>
|
||||
<string name="reload_mint_available">%1$s sats 可用</string>
|
||||
<string name="reload_mint_confirm">充值 & 发送打闪</string>
|
||||
<string name="reload_mint_send_confirm">发送打闪</string>
|
||||
<string name="reload_mint_awaiting_payment">等待支付中…</string>
|
||||
<string name="reload_mint_retry">重试</string>
|
||||
<string name="reload_mint_summary">充值 %1$s sats 到 %2$s 并打闪 %3$s %4$s sats · fee ≈ %5$s sats</string>
|
||||
<string name="reload_mint_summary_funded">从 %3$s 打闪 %1$s %2$s sats</string>
|
||||
<string name="reload_mint_recipient_fallback">接收者</string>
|
||||
<string name="reload_mint_needs_more">需要另外 %1$s sat</string>
|
||||
<string name="reload_mint_funded">已支付</string>
|
||||
<string name="reload_mint_copy_invoice">复制发票</string>
|
||||
<string name="topup_mint_title">铸币厂充值</string>
|
||||
<string name="topup_mint_action">向该铸币厂充值</string>
|
||||
<string name="topup_mint_amount_label">要添加的金额</string>
|
||||
<string name="topup_mint_confirm">充值</string>
|
||||
<string name="zap_privacy_section">打闪隐私</string>
|
||||
<string name="zap_type_section_explainer">控制发送打闪时如何显示您的身份。</string>
|
||||
<string name="wallet_connect_connect_app">连接钱包</string>
|
||||
@@ -1810,7 +1839,6 @@
|
||||
<string name="wallet_invalid_uri">无效的 NWC 连接 URI</string>
|
||||
<string name="wallet_move_up">上移</string>
|
||||
<string name="wallet_move_down">下移</string>
|
||||
<string name="wallet_reorder">调整钱包顺序</string>
|
||||
<string name="wallet_onchain_transactions">链上交易</string>
|
||||
<string name="wallet_onchain_no_address">此账户没有链上地址可用。</string>
|
||||
<string name="wallet_onchain_no_backend">没有配置链后端。</string>
|
||||
|
||||
@@ -806,6 +806,13 @@
|
||||
<string name="remove_from_private_bookmarks">Remove from Private Bookmarks</string>
|
||||
<string name="remove_from_public_bookmarks">Remove from Public Bookmarks</string>
|
||||
|
||||
<string name="add_hashtag_label">Add hashtag</string>
|
||||
<string name="add_hashtag_label_title">Add a hashtag</string>
|
||||
<string name="add_hashtag_label_explainer">Publicly tag this post with a hashtag (NIP-32 label). People who follow you will see it in that hashtag\'s feed.</string>
|
||||
<string name="add_hashtag_label_field">Hashtag</string>
|
||||
<string name="add_hashtag_label_confirm">Add</string>
|
||||
<string name="hashtag_label_added_by">added by</string>
|
||||
|
||||
<string name="pinned_notes">Pinned Notes</string>
|
||||
<string name="pinned_notes_explainer">Your pinned notes</string>
|
||||
<string name="pin_to_profile">Pin to Profile</string>
|
||||
@@ -897,6 +904,10 @@
|
||||
<string name="reload_mint_needs_more">needs %1$s sat more</string>
|
||||
<string name="reload_mint_funded">funded</string>
|
||||
<string name="reload_mint_copy_invoice">Copy invoice</string>
|
||||
<string name="topup_mint_title">Top up mint</string>
|
||||
<string name="topup_mint_action">Top up this mint</string>
|
||||
<string name="topup_mint_amount_label">Amount to add</string>
|
||||
<string name="topup_mint_confirm">Top up</string>
|
||||
<string name="zap_privacy_section">Zap Privacy</string>
|
||||
<string name="zap_type_section_explainer">Controls how your identity is shown when you send a zap.</string>
|
||||
<string name="wallet_connect_connect_app">Connect Wallet</string>
|
||||
@@ -2051,7 +2062,6 @@
|
||||
<string name="wallet_invalid_uri">Invalid NWC connection URI</string>
|
||||
<string name="wallet_move_up">Move Up</string>
|
||||
<string name="wallet_move_down">Move Down</string>
|
||||
<string name="wallet_reorder">Reorder wallet</string>
|
||||
<string name="wallet_onchain_transactions">Onchain Transactions</string>
|
||||
<string name="wallet_onchain_no_address">No on-chain address available for this account.</string>
|
||||
<string name="wallet_onchain_no_backend">No chain backend is configured.</string>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import com.vitorpamplona.amethyst.service.firstFullChar
|
||||
import com.vitorpamplona.amethyst.commons.util.firstFullChar
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
|
||||
@@ -145,6 +145,7 @@ open class Note(
|
||||
removeZap(note)
|
||||
removeZapPayment(note)
|
||||
removeReport(note)
|
||||
removeLabel(note)
|
||||
}
|
||||
|
||||
var poll: PollResponsesCache? = null
|
||||
@@ -166,6 +167,16 @@ open class Note(
|
||||
var reports = mapOf<User, List<Note>>()
|
||||
private set
|
||||
|
||||
/**
|
||||
* NIP-32 hashtag labels (kind 1985) targeting this note.
|
||||
* Key: hashtag value, lowercased (the `l` tag value under the `#t` namespace).
|
||||
* Value: the list of LabelEvent notes that applied that hashtag to this note,
|
||||
* so `source.author` identifies who labeled it. Used by the hashtag feed to
|
||||
* surface posts a follow has tagged and to attribute the label in the UI.
|
||||
*/
|
||||
var labels = mapOf<String, List<Note>>()
|
||||
private set
|
||||
|
||||
var zaps = mapOf<Note, Note?>()
|
||||
private set
|
||||
|
||||
@@ -366,12 +377,14 @@ open class Note(
|
||||
val zapsChanged = zaps.isNotEmpty() || zapPayments.isNotEmpty() || onchainZaps.isNotEmpty() || nutzaps.isNotEmpty()
|
||||
val boostsChanged = boosts.isNotEmpty()
|
||||
val reportsChanged = reports.isNotEmpty()
|
||||
val labelsChanged = labels.isNotEmpty()
|
||||
|
||||
val toBeRemoved =
|
||||
replies +
|
||||
reactions.values.flatten() +
|
||||
boosts +
|
||||
reports.values.flatten() +
|
||||
labels.values.flatten() +
|
||||
zaps.keys +
|
||||
zaps.values.filterNotNull() +
|
||||
zapPayments.keys +
|
||||
@@ -382,6 +395,7 @@ open class Note(
|
||||
reactions = mapOf()
|
||||
boosts = listOf()
|
||||
reports = mapOf()
|
||||
labels = mapOf()
|
||||
zaps = mapOf()
|
||||
onchainZaps = mapOf()
|
||||
onchainZapResolved = false
|
||||
@@ -394,6 +408,7 @@ open class Note(
|
||||
if (reactionsChanged) flowSet?.reactions?.invalidateData()
|
||||
if (boostsChanged) flowSet?.boosts?.invalidateData()
|
||||
if (reportsChanged) flowSet?.reports?.invalidateData()
|
||||
if (labelsChanged) flowSet?.labels?.invalidateData()
|
||||
if (zapsChanged) flowSet?.zaps?.invalidateData()
|
||||
|
||||
return toBeRemoved
|
||||
@@ -668,6 +683,32 @@ open class Note(
|
||||
}
|
||||
}
|
||||
|
||||
/** Attach a NIP-32 LabelEvent note as having tagged this note with [hashtag] (lowercased). */
|
||||
fun addLabel(
|
||||
hashtag: String,
|
||||
note: Note,
|
||||
) {
|
||||
val listOfLabelers = labels[hashtag]
|
||||
if (listOfLabelers == null) {
|
||||
labels = labels + Pair(hashtag, listOf(note))
|
||||
flowSet?.labels?.invalidateData()
|
||||
} else if (!listOfLabelers.contains(note)) {
|
||||
labels = labels + Pair(hashtag, listOfLabelers + note)
|
||||
flowSet?.labels?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
/** Detach a LabelEvent note (e.g. deleted) from every hashtag bucket it was in. */
|
||||
fun removeLabel(note: Note) {
|
||||
if (labels.none { it.value.contains(note) }) return
|
||||
|
||||
labels =
|
||||
labels
|
||||
.mapValues { it.value - note }
|
||||
.filterValues { it.isNotEmpty() }
|
||||
flowSet?.labels?.invalidateData()
|
||||
}
|
||||
|
||||
fun addRelaySync(relay: NormalizedRelayUrl) =
|
||||
syncLock.withLock {
|
||||
if (relay !in relays) {
|
||||
@@ -1239,6 +1280,7 @@ class NoteFlowSet(
|
||||
val zaps = NoteBundledRefresherFlow(u)
|
||||
val ots = NoteBundledRefresherFlow(u)
|
||||
val edits = NoteBundledRefresherFlow(u)
|
||||
val labels = NoteBundledRefresherFlow(u)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun author() =
|
||||
@@ -1257,7 +1299,8 @@ class NoteFlowSet(
|
||||
replies.hasObservers() ||
|
||||
zaps.hasObservers() ||
|
||||
ots.hasObservers() ||
|
||||
edits.hasObservers()
|
||||
edits.hasObservers() ||
|
||||
labels.hasObservers()
|
||||
}
|
||||
|
||||
@Stable
|
||||
|
||||
@@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.commons.model.nip01Core.UserRelaysCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.UserNip05Cache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.UserStatusCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip56Reports.UserReportCache
|
||||
import com.vitorpamplona.amethyst.commons.model.trustedAssertions.UserCardsCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.UserCardsCache
|
||||
import com.vitorpamplona.amethyst.commons.util.toShortDisplay
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip30CustomEmojis
|
||||
package com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
|
||||
+5
-3
@@ -18,15 +18,17 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip47WalletConnect
|
||||
package com.vitorpamplona.amethyst.commons.model.nip47WalletConnect
|
||||
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
@Serializable
|
||||
data class NwcWalletEntry(
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
val id: String = Uuid.random().toString(),
|
||||
val name: String,
|
||||
val uri: Nip47WalletConnect.Nip47URI,
|
||||
) {
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.favoriteAlgoFeedsLists
|
||||
package com.vitorpamplona.amethyst.commons.model.nip51Lists.favoriteAlgoFeedsLists
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists
|
||||
package com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.interestSets
|
||||
package com.vitorpamplona.amethyst.commons.model.nip51Lists.interestSets
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists
|
||||
package com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.muteList
|
||||
package com.vitorpamplona.amethyst.commons.model.nip51Lists.muteList
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists.peopleList
|
||||
package com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.cashu
|
||||
package com.vitorpamplona.amethyst.commons.model.nip60Cashu
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import kotlinx.serialization.Serializable
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip72Communities
|
||||
package com.vitorpamplona.amethyst.commons.model.nip72Communities
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.trustedAssertions
|
||||
package com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.trustedAssertions
|
||||
package com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions
|
||||
|
||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.tags.ServiceProviderTag
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.trustedAssertions
|
||||
package com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip30CustomEmojis
|
||||
package com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis
|
||||
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
|
||||
@@ -34,6 +35,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.eTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.toETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.pTag
|
||||
@@ -82,6 +84,16 @@ class LabelEvent(
|
||||
/** Labels filtered by a specific namespace. */
|
||||
fun labelsByNamespace(namespace: String) = labels().filter { it.namespace == namespace }
|
||||
|
||||
/**
|
||||
* Hashtags this event associates with its targets, via the NIP-32 tag-association
|
||||
* namespace `#t` (`["L", "#t"]` + `["l", "<hashtag>", "#t"]`). Values are returned
|
||||
* lowercased so callers can compare without worrying about case.
|
||||
*/
|
||||
fun hashtagAssociations() =
|
||||
labelsByNamespace(HASHTAG_NAMESPACE)
|
||||
.map { it.label.lowercase() }
|
||||
.distinct()
|
||||
|
||||
/** Referenced event IDs (label targets). */
|
||||
fun labeledEvents() = tags.mapNotNull(ETag::parseId)
|
||||
|
||||
@@ -104,6 +116,30 @@ class LabelEvent(
|
||||
const val KIND = 1985
|
||||
const val ALT = "Label event"
|
||||
|
||||
/**
|
||||
* NIP-32 tag-association namespace for hashtags. A label of the form
|
||||
* `["l", "<hashtag>", "#t"]` (with `["L", "#t"]`) associates the target
|
||||
* with the hashtag `<hashtag>` under the standard `t` tag.
|
||||
*/
|
||||
const val HASHTAG_NAMESPACE = "#t"
|
||||
|
||||
/**
|
||||
* Build a label event that tags an existing event with a hashtag, using the
|
||||
* NIP-32 tag-association namespace `#t`. The hashtag is stored lowercased so
|
||||
* follow-graph hashtag feeds match regardless of input case.
|
||||
*/
|
||||
fun buildHashtagLabel(
|
||||
labeledEvent: EventHintBundle<out Event>,
|
||||
hashtag: String,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
) = eventTemplate<LabelEvent>(KIND, "", createdAt) {
|
||||
alt(ALT)
|
||||
eTag(labeledEvent.toETag())
|
||||
val tag = LabelTag(hashtag.removePrefix("#").lowercase(), HASHTAG_NAMESPACE)
|
||||
labelNamespace(tag.namespace)
|
||||
label(tag)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a label event for labeling events.
|
||||
*/
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip32Labeling
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip32Labeling.tags.LabelTag
|
||||
import com.vitorpamplona.quartz.utils.EventFactory
|
||||
@@ -160,6 +161,61 @@ class LabelEventTest {
|
||||
assertEquals("en", languageLabels[0].label)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBuildHashtagLabel() {
|
||||
val labeledEvent =
|
||||
EventFactory.create<Event>(
|
||||
id = testEventId,
|
||||
pubKey = testPubKey,
|
||||
createdAt = 1234567890L,
|
||||
kind = 1,
|
||||
tags = arrayOf(),
|
||||
content = "hello",
|
||||
sig = "c".repeat(128),
|
||||
)
|
||||
|
||||
val template =
|
||||
LabelEvent.buildHashtagLabel(
|
||||
labeledEvent = EventHintBundle(labeledEvent),
|
||||
hashtag = "#PhotoGraphy",
|
||||
)
|
||||
|
||||
assertEquals(1985, template.kind)
|
||||
|
||||
val hasEventRef = template.tags.any { it[0] == "e" && it[1] == testEventId }
|
||||
assertTrue(hasEventRef, "Should have e tag targeting labeled event")
|
||||
|
||||
val hasNamespace = template.tags.any { it[0] == "L" && it[1] == "#t" }
|
||||
assertTrue(hasNamespace, "Should have the #t tag-association namespace")
|
||||
|
||||
// The leading '#' is stripped and the value is lowercased.
|
||||
val hasLabel = template.tags.any { it[0] == "l" && it[1] == "photography" && it[2] == "#t" }
|
||||
assertTrue(hasLabel, "Should have a lowercased l label tag under #t")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHashtagAssociations() {
|
||||
val event =
|
||||
EventFactory.create<LabelEvent>(
|
||||
id = testEventId,
|
||||
pubKey = testPubKey,
|
||||
createdAt = 1234567890L,
|
||||
kind = 1985,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("L", "#t"),
|
||||
arrayOf("l", "Bitcoin", "#t"),
|
||||
arrayOf("l", "nostr", "#t"),
|
||||
arrayOf("l", "MIT", "license"),
|
||||
arrayOf("e", testEventId),
|
||||
),
|
||||
content = "",
|
||||
sig = "c".repeat(128),
|
||||
)
|
||||
|
||||
assertEquals(listOf("bitcoin", "nostr"), event.hashtagAssociations())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLabeledTargets() {
|
||||
val event =
|
||||
|
||||
Reference in New Issue
Block a user