Merge branch 'main' into claude/nip42-relay-auth-P878m
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
---
|
||||
name: find-missing-translations
|
||||
description: Use when comparing Android strings.xml locale files to find untranslated string resources, missing translation keys, or preparing translation work for a specific language
|
||||
---
|
||||
|
||||
# Find Missing Translations
|
||||
|
||||
## Overview
|
||||
|
||||
Extract string resource keys from the default `values/strings.xml` that are absent in a target locale's `strings.xml`, excluding non-translatable entries. Outputs a table ready for translation.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Need to find untranslated strings for a specific locale
|
||||
- Preparing a batch of strings for a translator
|
||||
- Checking translation coverage after adding new features
|
||||
|
||||
## Technique
|
||||
|
||||
### 1. Identify files
|
||||
|
||||
```
|
||||
Default: amethyst/src/main/res/values/strings.xml
|
||||
Target: amethyst/src/main/res/values-<locale>/strings.xml
|
||||
```
|
||||
|
||||
Default locale: `cs-rCZ` if none specified. User may override (e.g., `pt-rBR`, `ja`).
|
||||
|
||||
### 2. Extract and diff keys
|
||||
|
||||
Use a single bash pipeline to extract translatable keys from both files and diff them:
|
||||
|
||||
```bash
|
||||
# Extract translatable keys from default (exclude translatable="false")
|
||||
comm -23 \
|
||||
<(grep '<string name=' amethyst/src/main/res/values/strings.xml \
|
||||
| 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)
|
||||
```
|
||||
|
||||
This gives the list of missing key names.
|
||||
|
||||
### 3. Get English values for missing keys
|
||||
|
||||
For each missing key, extract its English value:
|
||||
|
||||
```bash
|
||||
# For each missing key, extract the full line from default strings.xml
|
||||
while IFS= read -r key; do
|
||||
grep "name=\"$key\"" amethyst/src/main/res/values/strings.xml
|
||||
done < <(comm -23 \
|
||||
<(grep '<string name=' amethyst/src/main/res/values/strings.xml \
|
||||
| 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))
|
||||
```
|
||||
|
||||
### 4. Present results
|
||||
|
||||
Output the missing entries as raw XML resource lines (copy-paste ready for the locale file):
|
||||
|
||||
```xml
|
||||
<string name="attestation_valid">Valid</string>
|
||||
<string name="attestation_valid_from">Valid from %1$s</string>
|
||||
<string name="feed_group_lists">Lists</string>
|
||||
```
|
||||
|
||||
Also check `<string-array>` and `<plurals>` tags using the same approach if the project uses them.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
- **Forgetting `translatable="false"`** — these should never appear in locale files
|
||||
- **Not checking string-arrays/plurals** — only checking `<string>` misses other resource types
|
||||
- **Modifying files** — this is a read-only research task unless the user asks to add entries
|
||||
+47
-14
@@ -1,4 +1,9 @@
|
||||
<a id="v1.06.0"></a>
|
||||
# [Release v1.06.0: Polls, Relay Feeds, Wallets and much more](https://github.com/vitorpamplona/amethyst/releases/tag/v1.06.0) - 2025-03-21
|
||||
|
||||
Adds support for creating and rendering NIP-85 Polls
|
||||
- Redesign of the poll and zap poll cards
|
||||
- Adds Special notification card that stays while the poll is running
|
||||
|
||||
Adds support for Relay Feeds
|
||||
- Adds support for NIP-51 favorite relay feeds
|
||||
@@ -15,7 +20,10 @@ Redesigns Media Player
|
||||
- Turn video controller creation into a flow to fix playback lifecycle issues
|
||||
- Adds support for uploading audio
|
||||
|
||||
Adds support for NIP-47 Wallets
|
||||
Adds support for NIP-47 Wallets and compete NWC spec
|
||||
- Adds views for Balance and Transactions
|
||||
- Add transaction filtering and pagination to wallet screen
|
||||
- Added several test cases from other repos to guarantee interoperability
|
||||
|
||||
Adds support for NIP-52 Calendar appointments
|
||||
|
||||
@@ -29,30 +37,32 @@ Adds support for NIP-A3 Payment targets (PayTo: 10133) by @npub1w4uswmv6lu9yel00
|
||||
|
||||
Adds support for BUD-10 "Blossom:" URIs in images, audios, videos, and documents.
|
||||
|
||||
Adds support for NIP-40 Expirations in any new post.
|
||||
Adds support for custom NIP-40 Expirations in any new post.
|
||||
- Displays expirations on posts and DMs
|
||||
|
||||
Adds support for NIP-66 Relay Monitor and discovery support to Quartz
|
||||
|
||||
Adds support for Namecoin .bit urls to NIP-05
|
||||
- Adds choice of ElectrumX server to resolve namecoins.
|
||||
Adds support for Attestations (https://attestr.xyz/)
|
||||
|
||||
Adds basic support for Chess with Jester protocol
|
||||
|
||||
Adds NIP-46 Bunker support to Quartz and Amethyst Desktop
|
||||
|
||||
Adds a Broadcasting feedback pop-up in the Complete UI mode
|
||||
Adds support for inline reply, mark as read from Push Notifications
|
||||
|
||||
Adds support for rendering Zap events when quoted inside of posts.
|
||||
Removes NIP-04 DMs and blocks DM sending if the receiver doesn't have NIP-17 relay lists.
|
||||
|
||||
Removes support for NIP-96 and updates Blossom recommendations
|
||||
|
||||
Adds support to upload Documents to all new post screens.
|
||||
Uploads:
|
||||
- Adds support to upload Documents to all new post screens.
|
||||
- Adds toggle to stip file metadata regardless of compression by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef
|
||||
- Adds encrypted file upload fallback option for NIP-17 chats
|
||||
- Removes support for NIP-96 and updates Blossom recommendations
|
||||
|
||||
Content warning improvements:
|
||||
- Adds optional description field for sensitive content warnings in new posts.
|
||||
- Adds an optional description field for sensitive content warnings in new posts.
|
||||
- Displays additional information on warning composables
|
||||
|
||||
Redesigns and reorganizes Setting pages
|
||||
Settings redesign
|
||||
- Consolidate drawer settings into a single Settings hub screen
|
||||
- Redesigns Zap Amount and NWC setup screens
|
||||
- Redesigns Custom zap amount screens
|
||||
@@ -67,7 +77,14 @@ URL/URI parser rewrite in Kotlin multiplatform (KMP)
|
||||
- Treat multibyte characters as URL terminators in RichTextParser by @npub1k0jrarx8um0lyw3nmysn50539ky4k8p7gfgzgrsvn8d7lccx3d0s38dczd
|
||||
- Adds a parser for blossom: uris
|
||||
|
||||
Minimizes parent thread rendering in quoted notes by @npub1aeh2zw4elewy5682lxc6xnlqzjnxksq303gwu2npfaxd49vmde6qcq4nwx
|
||||
UI Improvements:
|
||||
- Minimizes parent thread rendering in quoted notes by @npub1aeh2zw4elewy5682lxc6xnlqzjnxksq303gwu2npfaxd49vmde6qcq4nwx
|
||||
- New UI for DropDowns by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef
|
||||
- New UI for feed filters by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef
|
||||
- Draft Screen requests confirmation before deleting drafts on swipe
|
||||
- Swipe to switch tabs. Main screen and messages by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef
|
||||
- Adds support for rendering Zap events when quoted inside of posts.
|
||||
- Adds a Broadcasting feedback pop-up in the Complete UI mode
|
||||
|
||||
Relay Management:
|
||||
- Adds relay search tooltip when adding relays
|
||||
@@ -75,7 +92,9 @@ Relay Management:
|
||||
- Adds active subscriptions and outbox event in the queue to relay information
|
||||
- Adds a complete list of event kind names to the subscription card to relay information
|
||||
- Tracks and displays connection success rate on relay settings
|
||||
- Add relay settings export functionality
|
||||
- Adds relay settings export functionality
|
||||
- Adds NIP-45 count queries to show how many events each relay has.
|
||||
- Adds Relay sync utility to help users move posts between relays.
|
||||
|
||||
Search fixes
|
||||
- Breaks the search filter into two subscriptions to prioritize Metadata without punishing content.
|
||||
@@ -88,11 +107,14 @@ Search fixes
|
||||
|
||||
Profiles:
|
||||
- Adds a profile picture upload button when the user has no picture
|
||||
- Adds last seen to the user profile
|
||||
- Adds nprofile and npub copy options to the profile
|
||||
- Groups received zap amounts by sending the user in the profile tab
|
||||
- Increases the limit of Zap downloads for profiles to 1000
|
||||
- Simplifies profile edit screen layout by @npub1aeh2zw4elewy5682lxc6xnlqzjnxksq303gwu2npfaxd49vmde6qcq4nwx
|
||||
- Migrates profile galleries to display a thumbnail for videos
|
||||
- Fixes profile galleries' aspect ratios
|
||||
- Adds support for Namecoin .bit urls to NIP-05 and choice of ElectrumX server to resolve namecoins.
|
||||
|
||||
Bulk Follow onboarding
|
||||
- Adds screens to search for a user and to copy his/her follow list
|
||||
@@ -126,6 +148,7 @@ Fixes:
|
||||
- Fixes bug on Show More calculations for very long texts without spaces
|
||||
- Fixing IO Dispatchers and coroutine scopes of choice
|
||||
- Fixes anySync parallel operation that was returning the first result, not the first positive "any".
|
||||
- Fixes Req onCannotConnect listeners to the relays that actually sent the req
|
||||
|
||||
AI:
|
||||
- Add SKILL.md for AI agent customization
|
||||
@@ -137,6 +160,14 @@ Defaults:
|
||||
- Adds wss://directory.yabu.me and wss://profiles.nostr1.com as index relays
|
||||
- Adds electrumx.testls.space, nmc2.bitcoins.sk, 46.229.238.187 and i665jpwsq46zlsdbnj4axgzd3s56uzey5uhotsnxzsknzbn36jaddsid.onion as ElectrumX servers
|
||||
|
||||
Quartz:
|
||||
- Adds Relay Server implementation with NIP-45 COUNT and NIP-42 AUTH support
|
||||
- Adds support for dynamic policies to the relay implementation.
|
||||
- Migrates Quartz EventStore from Android-only to KMP
|
||||
- Adds a reqUntilEoseAsFlow extension to the Nostr Client
|
||||
- Adds comprehensive NIP-46 Bunker support
|
||||
- Adds comprehensive support for NIP-47 non-payment methods.
|
||||
|
||||
Adds complete support for iOS to Quartz by @npub1a3tx8wcrt789skl6gg7rqwj4wey0j53eesr4z6asd4h4jwrd62jq0wkq4k
|
||||
- Provide implementation for Rfc3986 on iOS, using the Swift Rfc3986UriBridge.
|
||||
- Provide implementation for LargeCache, using a CacheMap
|
||||
@@ -149,7 +180,6 @@ Adds complete support for iOS to Quartz by @npub1a3tx8wcrt789skl6gg7rqwj4wey0j53
|
||||
- Provide implementation for AESGCM
|
||||
- Provide implementation for DigestInstance
|
||||
- Provide implementation for LibSodium
|
||||
- Adds a reqUntilEoseAsFlow extension to the Nostr Client
|
||||
|
||||
Amethyst Desktop by @npub12cfje6nl2nuxplcqfvhg7ljt89fmpj0n0fd24zxsukja5qm9wmtqd7y76c
|
||||
- Adds NIP-46 Bunker Login
|
||||
@@ -159,6 +189,8 @@ Amethyst Desktop by @npub12cfje6nl2nuxplcqfvhg7ljt89fmpj0n0fd24zxsukja5qm9wmtqd7
|
||||
- Adds encrypted DMs (NIP-04/NIP-17)
|
||||
- Adds proper empty states with EOSE tracking
|
||||
- Adds multi-column deck layout
|
||||
- Adds Full media parity — images, video, audio, encrypted DMs, upload, lightbox
|
||||
- Adds advanced search with NIP-50, collapsible sections, and nav state preservation
|
||||
- Clear stored credentials on logout
|
||||
- Adds bunker heartbeat indicator
|
||||
- Adds QR-based signer pairing
|
||||
@@ -176,6 +208,7 @@ Amethyst Desktop by @npub12cfje6nl2nuxplcqfvhg7ljt89fmpj0n0fd24zxsukja5qm9wmtqd7
|
||||
|
||||
Code Quality
|
||||
- Migrates to AGP 9.0
|
||||
- Adds Amethyst Desktop to CI/CD and Release builds
|
||||
- Removes the in-app memory counter methods
|
||||
- Refactors the old NIP-05 code on Quartz
|
||||
- Migrates contact list management to addressable notes
|
||||
|
||||
@@ -39,6 +39,10 @@ import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
|
||||
import com.vitorpamplona.amethyst.service.BundledInsert
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.note.dateFormatter
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
|
||||
@@ -619,6 +623,30 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
wasVerified: Boolean,
|
||||
) = consumeBaseReplaceable(event, relay, wasVerified)
|
||||
|
||||
fun consume(
|
||||
event: AttestationEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
) = consumeBaseReplaceable(event, relay, wasVerified)
|
||||
|
||||
fun consume(
|
||||
event: AttestationRequestEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
) = consumeBaseReplaceable(event, relay, wasVerified)
|
||||
|
||||
fun consume(
|
||||
event: AttestorRecommendationEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
) = consumeBaseReplaceable(event, relay, wasVerified)
|
||||
|
||||
fun consume(
|
||||
event: AttestorProficiencyEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
) = consumeBaseReplaceable(event, relay, wasVerified)
|
||||
|
||||
fun consumeRegularEvent(
|
||||
event: Event,
|
||||
relay: NormalizedRelayUrl?,
|
||||
@@ -3051,6 +3079,10 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
is AppDefinitionEvent -> consume(event, relay, wasVerified)
|
||||
is AppRecommendationEvent -> consume(event, relay, wasVerified)
|
||||
is AppSpecificDataEvent -> consume(event, relay, wasVerified)
|
||||
is AttestationEvent -> consume(event, relay, wasVerified)
|
||||
is AttestationRequestEvent -> consume(event, relay, wasVerified)
|
||||
is AttestorRecommendationEvent -> consume(event, relay, wasVerified)
|
||||
is AttestorProficiencyEvent -> consume(event, relay, wasVerified)
|
||||
is AudioHeaderEvent -> consume(event, relay, wasVerified)
|
||||
is AudioTrackEvent -> consume(event, relay, wasVerified)
|
||||
is BadgeAwardEvent -> consume(event, relay, wasVerified)
|
||||
|
||||
+25
-46
@@ -32,12 +32,9 @@ import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.PictureInPicture
|
||||
import androidx.compose.material.icons.filled.SaveAlt
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -45,9 +42,11 @@ import androidx.compose.runtime.remember
|
||||
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.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
@@ -134,61 +133,41 @@ fun OverflowMenuButton(
|
||||
modifier = Size20Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = menuExpanded.value,
|
||||
onDismissRequest = { menuExpanded.value = false },
|
||||
containerColor = Color.Black.copy(alpha = 0.85f),
|
||||
if (menuExpanded.value) {
|
||||
M3ActionDialog(
|
||||
title = stringRes(R.string.playback_actions_dialog_title),
|
||||
onDismiss = { menuExpanded.value = false },
|
||||
) {
|
||||
M3ActionSection {
|
||||
if (showShare) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.share_or_save), color = Color.White) },
|
||||
onClick = {
|
||||
M3ActionRow(
|
||||
icon = Icons.Default.Share,
|
||||
text = stringRes(R.string.share_or_save),
|
||||
) {
|
||||
menuExpanded.value = false
|
||||
onShareClick()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Default.Share,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
if (showSave) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.download_to_phone), color = Color.White) },
|
||||
onClick = {
|
||||
M3ActionRow(
|
||||
icon = Icons.Default.SaveAlt,
|
||||
text = stringRes(R.string.download_to_phone),
|
||||
) {
|
||||
menuExpanded.value = false
|
||||
onSaveClick()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Default.SaveAlt,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
if (showPip) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.picture_in_picture), color = Color.White) },
|
||||
onClick = {
|
||||
M3ActionRow(
|
||||
icon = Icons.Default.PictureInPicture,
|
||||
text = stringRes(R.string.picture_in_picture),
|
||||
) {
|
||||
menuExpanded.value = false
|
||||
onPipClick()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Default.PictureInPicture,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-2
@@ -20,6 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
|
||||
@@ -85,6 +87,12 @@ val NotificationsPerKeyKinds2 =
|
||||
InteractiveStorySceneEvent.KIND,
|
||||
)
|
||||
|
||||
val NotificationsPerKeyKinds3 =
|
||||
listOf(
|
||||
AttestationRequestEvent.KIND,
|
||||
AttestorRecommendationEvent.KIND,
|
||||
)
|
||||
|
||||
fun filterSummaryNotificationsToPubkey(
|
||||
relay: NormalizedRelayUrl,
|
||||
pubkey: HexKey?,
|
||||
@@ -130,7 +138,17 @@ fun filterNotificationsToPubkey(
|
||||
Filter(
|
||||
kinds = NotificationsPerKeyKinds2,
|
||||
tags = mapOf("p" to listOf(pubkey)),
|
||||
limit = 500,
|
||||
limit = 200,
|
||||
since = since,
|
||||
),
|
||||
),
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = NotificationsPerKeyKinds3,
|
||||
tags = mapOf("p" to listOf(pubkey)),
|
||||
limit = 10,
|
||||
since = since,
|
||||
),
|
||||
),
|
||||
@@ -171,7 +189,17 @@ fun filterJustTheLatestNotificationsToPubkeyFromRandomRelays(
|
||||
Filter(
|
||||
kinds = NotificationsPerKeyKinds2,
|
||||
tags = mapOf("p" to listOf(pubkey)),
|
||||
limit = 20,
|
||||
limit = 10,
|
||||
since = since,
|
||||
),
|
||||
),
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = NotificationsPerKeyKinds3,
|
||||
tags = mapOf("p" to listOf(pubkey)),
|
||||
limit = 2,
|
||||
since = since,
|
||||
),
|
||||
),
|
||||
|
||||
+2
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
@@ -47,6 +48,7 @@ val RepliesAndReactionsToAddressesKinds1 =
|
||||
LnZapEvent.KIND,
|
||||
ZapPollEvent.KIND,
|
||||
CommentEvent.KIND,
|
||||
AttestationEvent.KIND,
|
||||
)
|
||||
|
||||
val PostsAndChatMessagesToAddresses =
|
||||
|
||||
+2
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
@@ -53,6 +54,7 @@ val RepliesAndReactionsKinds =
|
||||
OtsEvent.KIND,
|
||||
TextNoteModificationEvent.KIND,
|
||||
CommentEvent.KIND,
|
||||
AttestationEvent.KIND,
|
||||
)
|
||||
|
||||
val RepliesAndReactionsKinds2 =
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
|
||||
@Composable
|
||||
fun M3ActionDialog(
|
||||
title: String,
|
||||
onDismiss: () -> Unit,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(vertical = 20.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 8.dp),
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun M3ActionSection(content: @Composable ColumnScope.() -> Unit) {
|
||||
Surface(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
) {
|
||||
Column {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun M3ActionRow(
|
||||
icon: ImageVector,
|
||||
text: String,
|
||||
isDestructive: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val tint =
|
||||
if (isDestructive) {
|
||||
MaterialTheme.colorScheme.error
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
val textColor =
|
||||
if (isDestructive) {
|
||||
MaterialTheme.colorScheme.error
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
val alpha = if (enabled) 1f else 0.38f
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.alpha(alpha)
|
||||
.clickable(enabled = enabled, role = Role.Button, onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Size20Modifier,
|
||||
tint = tint,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = Font14SP,
|
||||
color = textColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,11 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -47,27 +46,6 @@ import com.vitorpamplona.amethyst.ui.theme.MaxWidthWithHorzPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.previewCardImageModifier
|
||||
|
||||
@Composable
|
||||
private fun CopyToClipboard(
|
||||
popupExpanded: MutableState<Boolean>,
|
||||
content: String,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = popupExpanded.value,
|
||||
onDismissRequest = onDismiss,
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.copy_url_to_clipboard)) },
|
||||
onClick = {
|
||||
clipboardManager.setText(AnnotatedString(content))
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun UrlPreviewCard(
|
||||
@@ -81,13 +59,22 @@ fun UrlPreviewCard(
|
||||
}
|
||||
|
||||
if (popupExpanded.value) {
|
||||
CopyToClipboard(
|
||||
popupExpanded = popupExpanded,
|
||||
content = url,
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
M3ActionDialog(
|
||||
title = stringRes(R.string.link_actions_dialog_title),
|
||||
onDismiss = { popupExpanded.value = false },
|
||||
) {
|
||||
M3ActionSection {
|
||||
M3ActionRow(
|
||||
icon = Icons.Outlined.ContentCopy,
|
||||
text = stringRes(R.string.copy_url_to_clipboard),
|
||||
) {
|
||||
clipboardManager.setText(AnnotatedString(url))
|
||||
popupExpanded.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
|
||||
+32
-49
@@ -32,15 +32,16 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Report
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material.icons.outlined.Collections
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material.icons.outlined.Link
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -771,63 +772,54 @@ fun ShareMediaAction(
|
||||
// Track if video is downloading - hoisted here to block menu dismiss during download
|
||||
val isDownloadingVideo = remember { mutableStateOf(false) }
|
||||
|
||||
DropdownMenu(
|
||||
expanded = popupExpanded.value,
|
||||
onDismissRequest = { if (!isDownloadingVideo.value) onDismiss() },
|
||||
if (popupExpanded.value) {
|
||||
M3ActionDialog(
|
||||
title = stringRes(R.string.media_actions_dialog_title),
|
||||
onDismiss = { if (!isDownloadingVideo.value) onDismiss() },
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
// Copy & Gallery section
|
||||
if ((videoUri != null && !videoUri.startsWith("file")) || postNostrUri != null) {
|
||||
M3ActionSection {
|
||||
if (videoUri != null && !videoUri.startsWith("file")) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.copy_url_to_clipboard)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.Link, text = stringRes(R.string.copy_url_to_clipboard)) {
|
||||
clipboardManager.setText(AnnotatedString(videoUri))
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
postNostrUri?.let {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.copy_the_note_id_to_the_clipboard)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_the_note_id_to_the_clipboard)) {
|
||||
clipboardManager.setText(AnnotatedString(it))
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
postNostrUri?.let {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.add_media_to_gallery)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.Collections, text = stringRes(R.string.add_media_to_gallery)) {
|
||||
if (videoUri != null) {
|
||||
val n19 = Nip19Parser.uriToRoute(postNostrUri)?.entity as? NEvent
|
||||
if (n19 != null) {
|
||||
accountViewModel.addMediaToGallery(n19.hex, videoUri, n19.relay.getOrNull(0), blurhash, dim, hash, mimeType) // TODO Whole list or first?
|
||||
accountViewModel.addMediaToGallery(n19.hex, videoUri, n19.relay.getOrNull(0), blurhash, dim, hash, mimeType)
|
||||
accountViewModel.toastManager.toast(R.string.media_added, R.string.media_added_to_profile_gallery)
|
||||
}
|
||||
}
|
||||
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Share section
|
||||
content?.let {
|
||||
val context = LocalContext.current
|
||||
|
||||
M3ActionSection {
|
||||
when (content) {
|
||||
is MediaUrlImage -> {
|
||||
videoUri?.let {
|
||||
if (videoUri.isNotEmpty()) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.share_image)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.Share, text = stringRes(R.string.share_image)) {
|
||||
scope.launch { shareImageFile(context, videoUri, mimeType) }
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -835,18 +827,11 @@ fun ShareMediaAction(
|
||||
is MediaUrlVideo -> {
|
||||
videoUri?.let {
|
||||
if (videoUri.isNotEmpty()) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(stringRes(R.string.share_video))
|
||||
if (isDownloadingVideo.value) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
LoadingAnimation(indicatorSize = 16.dp, circleWidth = 2.dp)
|
||||
}
|
||||
}
|
||||
},
|
||||
M3ActionRow(
|
||||
icon = Icons.Outlined.Share,
|
||||
text = stringRes(R.string.share_video),
|
||||
enabled = !isDownloadingVideo.value,
|
||||
onClick = {
|
||||
) {
|
||||
isDownloadingVideo.value = true
|
||||
scope.launch {
|
||||
shareVideoFile(
|
||||
@@ -865,21 +850,17 @@ fun ShareMediaAction(
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is MediaLocalVideo -> {
|
||||
content.localFile?.let { localFile ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.share_video)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.Share, text = stringRes(R.string.share_video)) {
|
||||
scope.launch { shareLocalVideoFile(context, localFile, mimeType) }
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -887,6 +868,8 @@ fun ShareMediaAction(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun shareImageFile(
|
||||
|
||||
@@ -102,6 +102,10 @@ import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.types.PictureDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAttestation
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAttestationRequest
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAttestorProficiency
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAttestorRecommendation
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAudioHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAudioTrack
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderBadgeAward
|
||||
@@ -174,6 +178,10 @@ import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.normalWithTopMarginNoteModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward
|
||||
@@ -767,6 +775,22 @@ private fun RenderNoteRow(
|
||||
RenderAppDefinition(baseNote, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is AttestationEvent -> {
|
||||
RenderAttestation(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is AttestationRequestEvent -> {
|
||||
RenderAttestationRequest(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is AttestorRecommendationEvent -> {
|
||||
RenderAttestorRecommendation(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is AttestorProficiencyEvent -> {
|
||||
RenderAttestorProficiency(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is AudioTrackEvent -> {
|
||||
RenderAudioTrack(baseNote, ContentScale.FillWidth, accountViewModel, nav)
|
||||
}
|
||||
|
||||
+90
-139
@@ -21,10 +21,22 @@
|
||||
package com.vitorpamplona.amethyst.ui.note.elements
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Bookmark
|
||||
import androidx.compose.material.icons.outlined.BookmarkAdd
|
||||
import androidx.compose.material.icons.outlined.BookmarkRemove
|
||||
import androidx.compose.material.icons.outlined.CellTower
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Lock
|
||||
import androidx.compose.material.icons.outlined.LockOpen
|
||||
import androidx.compose.material.icons.outlined.PersonAdd
|
||||
import androidx.compose.material.icons.outlined.PersonRemove
|
||||
import androidx.compose.material.icons.outlined.PlaylistAdd
|
||||
import androidx.compose.material.icons.outlined.Report
|
||||
import androidx.compose.material.icons.outlined.Schedule
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.State
|
||||
@@ -44,6 +56,9 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.EditPostView
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo
|
||||
@@ -53,7 +68,6 @@ import com.vitorpamplona.amethyst.ui.note.types.EditState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.report.ReportNoteDialog
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
@@ -78,6 +92,7 @@ fun MoreOptionsButton(
|
||||
onClick = { popupExpanded.value = true },
|
||||
) {
|
||||
VerticalDotsIcon()
|
||||
}
|
||||
|
||||
if (popupExpanded.value) {
|
||||
NoteDropDownMenu(
|
||||
@@ -88,7 +103,6 @@ fun MoreOptionsButton(
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@@ -146,213 +160,150 @@ fun NoteDropDownMenu(
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = true,
|
||||
onDismissRequest = onDismiss,
|
||||
M3ActionDialog(
|
||||
title = stringRes(R.string.note_actions_dialog_title),
|
||||
onDismiss = onDismiss,
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val actContext = LocalContext.current
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// Follow section
|
||||
M3ActionSection {
|
||||
if (!state.isFollowingAuthor) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.follow)) },
|
||||
onClick = {
|
||||
val author = note.author ?: return@DropdownMenuItem
|
||||
M3ActionRow(icon = Icons.Outlined.PersonAdd, text = stringRes(R.string.follow)) {
|
||||
val author = note.author ?: return@M3ActionRow
|
||||
accountViewModel.follow(author)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.unfollow)) },
|
||||
onClick = {
|
||||
val author = note.author ?: return@DropdownMenuItem
|
||||
M3ActionRow(icon = Icons.Outlined.PersonRemove, text = stringRes(R.string.unfollow)) {
|
||||
val author = note.author ?: return@M3ActionRow
|
||||
accountViewModel.unfollow(author)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringRes(R.string.follow_set_add_author_from_note_action)) },
|
||||
onClick = {
|
||||
val authorHexKey = note.author?.pubkeyHex ?: return@DropdownMenuItem
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.PlaylistAdd, text = stringRes(R.string.follow_set_add_author_from_note_action)) {
|
||||
val authorHexKey = note.author?.pubkeyHex ?: return@M3ActionRow
|
||||
nav.nav(Route.PeopleListManagement(authorHexKey))
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.copy_text)) },
|
||||
onClick = {
|
||||
val lastNoteVersion = (editState?.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value ?: note
|
||||
accountViewModel.decrypt(lastNoteVersion) {
|
||||
clipboardManager.setText(AnnotatedString(it))
|
||||
}
|
||||
}
|
||||
|
||||
// Copy & Share section
|
||||
M3ActionSection {
|
||||
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_text)) {
|
||||
val lastNoteVersion = (editState?.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value ?: note
|
||||
accountViewModel.decrypt(lastNoteVersion) { clipboardManager.setText(AnnotatedString(it)) }
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.copy_user_pubkey)) },
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_user_pubkey)) {
|
||||
note.author?.let {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(AnnotatedString("nostr:${it.pubkeyNpub()}"))
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.copy_note_id)) },
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_note_id)) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(AnnotatedString(note.toNostrUri()))
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.quick_action_share)) },
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.Share, text = stringRes(R.string.quick_action_share)) {
|
||||
val sendIntent =
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = "text/plain"
|
||||
putExtra(
|
||||
Intent.EXTRA_TEXT,
|
||||
externalLinkForNote(note),
|
||||
)
|
||||
putExtra(
|
||||
Intent.EXTRA_TITLE,
|
||||
stringRes(actContext, R.string.quick_action_share_browser_link),
|
||||
)
|
||||
putExtra(Intent.EXTRA_TEXT, externalLinkForNote(note))
|
||||
putExtra(Intent.EXTRA_TITLE, stringRes(actContext, R.string.quick_action_share_browser_link))
|
||||
}
|
||||
|
||||
val shareIntent =
|
||||
Intent.createChooser(sendIntent, stringRes(actContext, R.string.quick_action_share))
|
||||
val shareIntent = Intent.createChooser(sendIntent, stringRes(actContext, R.string.quick_action_share))
|
||||
ContextCompat.startActivity(actContext, shareIntent, null)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
if (state.isLoggedUser && note.isDraft()) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.edit_draft)) },
|
||||
onClick = {
|
||||
nav.nav {
|
||||
routeEditDraftTo(note, accountViewModel.account)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Edit & Broadcast section
|
||||
M3ActionSection {
|
||||
if (state.isLoggedUser && note.isDraft()) {
|
||||
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.edit_draft)) {
|
||||
nav.nav { routeEditDraftTo(note, accountViewModel.account) }
|
||||
}
|
||||
}
|
||||
if (note.event is TextNoteEvent && !note.isDraft()) {
|
||||
if (state.isLoggedUser) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.edit_post)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.edit_post)) {
|
||||
wantsToEditPost.value = true
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.propose_an_edit)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.propose_an_edit)) {
|
||||
wantsToEditPost.value = true
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.broadcast)) },
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.CellTower, text = stringRes(R.string.broadcast)) {
|
||||
accountViewModel.broadcast(note)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
|
||||
// Timestamp & Bookmarks section
|
||||
M3ActionSection {
|
||||
if (accountViewModel.account.otsState.hasPendingAttestations(note)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.timestamp_pending)) },
|
||||
onClick = {
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
M3ActionRow(icon = Icons.Outlined.Schedule, text = stringRes(R.string.timestamp_pending)) { onDismiss() }
|
||||
} else {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.timestamp_it)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.Schedule, text = stringRes(R.string.timestamp_it)) {
|
||||
accountViewModel.timestamp(note)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
note.let {
|
||||
}
|
||||
val noteBookmarkType = if (note.event is LongTextNoteEvent) stringRes(R.string.article) else stringRes(R.string.post)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.manage_bookmark_label, noteBookmarkType)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.BookmarkAdd, text = stringRes(R.string.manage_bookmark_label, noteBookmarkType)) {
|
||||
if (note.event is LongTextNoteEvent) {
|
||||
val noteAddress = (note as AddressableNote).address
|
||||
nav.nav(Route.ArticleBookmarkManagement(noteAddress))
|
||||
nav.nav(Route.ArticleBookmarkManagement((note as AddressableNote).address))
|
||||
} else {
|
||||
nav.nav(Route.PostBookmarkManagement(note.idHex))
|
||||
}
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
if (state.isPrivateBookmarkNote) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.remove_from_private_bookmarks)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.LockOpen, text = stringRes(R.string.remove_from_private_bookmarks)) {
|
||||
accountViewModel.removePrivateBookmark(note)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.add_to_private_bookmarks)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.Lock, text = stringRes(R.string.add_to_private_bookmarks)) {
|
||||
accountViewModel.addPrivateBookmark(note)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if (state.isPublicBookmarkNote) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.remove_from_public_bookmarks)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.BookmarkRemove, text = stringRes(R.string.remove_from_public_bookmarks)) {
|
||||
accountViewModel.removePublicBookmark(note)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.add_to_public_bookmarks)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.Bookmark, text = stringRes(R.string.add_to_public_bookmarks)) {
|
||||
accountViewModel.addPublicBookmark(note)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
|
||||
// Moderation section
|
||||
M3ActionSection {
|
||||
if (state.isLoggedUser) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.request_deletion)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.Delete, text = stringRes(R.string.request_deletion), isDestructive = true) {
|
||||
accountViewModel.delete(note)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.block_report)) },
|
||||
onClick = { reportDialogShowing = true },
|
||||
)
|
||||
M3ActionRow(icon = Icons.Outlined.Report, text = stringRes(R.string.block_report), isDestructive = true) {
|
||||
reportDialogShowing = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,638 @@
|
||||
/*
|
||||
* 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.types
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.HourglassTop
|
||||
import androidx.compose.material.icons.filled.Recommend
|
||||
import androidx.compose.material.icons.filled.Send
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material.icons.filled.VerifiedUser
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.UserCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.KindChip
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.AttestationStatus
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.Validity
|
||||
import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RenderAttestationPreview() {
|
||||
val event =
|
||||
AttestationEvent(
|
||||
id = "f8e05e4fa964d9dfbeb339ae0450d0b3424aaf8f6083d95f38c780335c3dbd56",
|
||||
pubKey = "c4f5e7a75a8ce3683d529cff06368439c529e5243c6b125ba68789198856cac7",
|
||||
createdAt = 1773941524,
|
||||
content = "This is Frank's new npub.",
|
||||
sig = "61dde5dc5738bfa637aa04451eec63d45f649902a0772abbaf55711aad5b7ce376bc7712c937f9ffc504655c4fd7e39f78139dc3f1a90953d150b43f3e2428fb",
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("d", "af5aa898:fe108febb997:1773941524"),
|
||||
arrayOf("e", "fe108febb99796c4091775e00aa1fc3ffc489ad22fdf1f8c559b2472815c09c7"),
|
||||
arrayOf("s", "verified"),
|
||||
arrayOf("v", "valid"),
|
||||
arrayOf("client", "attestr.xyz"),
|
||||
),
|
||||
)
|
||||
|
||||
LocalCache.justConsume(event, null, true)
|
||||
val note = LocalCache.getOrCreateNote(event.id)
|
||||
|
||||
ThemeComparisonColumn(
|
||||
toPreview = {
|
||||
RenderAttestation(
|
||||
baseNote = note,
|
||||
quotesLeft = 3,
|
||||
backgroundColor = remember { mutableStateOf(Color.Transparent) },
|
||||
accountViewModel = mockAccountViewModel(),
|
||||
nav = EmptyNav(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderAttestation(
|
||||
baseNote: Note,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteState by baseNote
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
val noteEvent = noteState.note.event as? AttestationEvent ?: return
|
||||
|
||||
RenderAttestation(
|
||||
baseNote,
|
||||
noteEvent,
|
||||
quotesLeft,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderAttestation(
|
||||
note: Note,
|
||||
noteEvent: AttestationEvent,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val validity = remember(noteEvent) { noteEvent.validity() }
|
||||
val status = remember(noteEvent) { noteEvent.status() }
|
||||
val validFrom = remember(noteEvent) { noteEvent.validFrom() }
|
||||
val validTo = remember(noteEvent) { noteEvent.validTo() }
|
||||
val content = remember(noteEvent) { noteEvent.content.ifBlank { null } }
|
||||
|
||||
val statusColor = remember(status, validity) { attestationColor(status, validity) }
|
||||
val statusIcon = remember(status, validity) { attestationIcon(status, validity) }
|
||||
val statusLabel = attestationStatusLabel(status, validity)
|
||||
|
||||
val aboutAddress = remember(noteEvent) { noteEvent.assertionAddress() }
|
||||
val aboutEvent = remember(noteEvent) { noteEvent.assertionEventId() }
|
||||
val aboutPubkey = remember(noteEvent) { noteEvent.assertionPubkey() }
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.border(1.dp, statusColor.copy(alpha = 0.3f), RoundedCornerShape(12.dp))
|
||||
.background(statusColor.copy(alpha = 0.06f))
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = statusIcon,
|
||||
contentDescription = stringRes(R.string.attestation),
|
||||
tint = statusColor,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
Text(
|
||||
text = statusLabel,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = statusColor,
|
||||
)
|
||||
}
|
||||
|
||||
if (validFrom != null || validTo != null) {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
validFrom?.let {
|
||||
Text(
|
||||
text = stringRes(R.string.attestation_valid_from, formatTimestamp(it)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
validTo?.let {
|
||||
Text(
|
||||
text = stringRes(R.string.attestation_valid_to, formatTimestamp(it)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (quotesLeft > 0) {
|
||||
if (aboutAddress != null) {
|
||||
LoadAddressableNote(aboutAddress, accountViewModel) {
|
||||
if (it != null) {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
NoteCompose(
|
||||
baseNote = it,
|
||||
modifier = MaterialTheme.colorScheme.replyModifier,
|
||||
isQuotedNote = true,
|
||||
unPackReply = ReplyRenderType.NONE,
|
||||
makeItShort = true,
|
||||
quotesLeft = quotesLeft - 1,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (aboutEvent != null) {
|
||||
LoadNote(aboutEvent, accountViewModel) {
|
||||
if (it != null) {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
NoteCompose(
|
||||
baseNote = it,
|
||||
modifier = MaterialTheme.colorScheme.replyModifier,
|
||||
isQuotedNote = true,
|
||||
unPackReply = ReplyRenderType.NONE,
|
||||
makeItShort = true,
|
||||
quotesLeft = quotesLeft - 1,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (aboutPubkey != null) {
|
||||
LoadUser(aboutPubkey, accountViewModel) {
|
||||
if (it != null) {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
UserCompose(it, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content?.let {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = true,
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tags = remember(note) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList },
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
callbackUri = note.toNostrUri(),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderAttestationRequest(
|
||||
note: Note,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteState by note
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
val noteEvent = noteState.note.event as? AttestationRequestEvent ?: return
|
||||
|
||||
val content = remember(noteEvent) { noteEvent.content.ifBlank { null } }
|
||||
|
||||
val aboutAddress = remember(noteEvent) { noteEvent.assertionAddress() }
|
||||
val aboutEvent = remember(noteEvent) { noteEvent.assertionEventId() }
|
||||
val aboutPubkey = remember(noteEvent) { noteEvent.assertionPubkey() }
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
|
||||
RoundedCornerShape(12.dp),
|
||||
).background(MaterialTheme.colorScheme.primary.copy(alpha = 0.06f))
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Send,
|
||||
contentDescription = stringRes(R.string.attestation_request),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.attestation_request),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
|
||||
content?.let {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = true,
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tags = remember(note) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList },
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
callbackUri = note.toNostrUri(),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (quotesLeft > 0) {
|
||||
if (aboutAddress != null) {
|
||||
LoadAddressableNote(aboutAddress, accountViewModel) {
|
||||
if (it != null) {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
Text(
|
||||
text = stringRes(R.string.attestation_requests_attestation_to),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
NoteCompose(
|
||||
baseNote = it,
|
||||
modifier = MaterialTheme.colorScheme.replyModifier,
|
||||
isQuotedNote = true,
|
||||
unPackReply = ReplyRenderType.NONE,
|
||||
makeItShort = true,
|
||||
quotesLeft = quotesLeft - 1,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (aboutEvent != null) {
|
||||
LoadNote(aboutEvent, accountViewModel) {
|
||||
if (it != null) {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
Text(
|
||||
text = stringRes(R.string.attestation_requests_attestation_to),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
NoteCompose(
|
||||
baseNote = it,
|
||||
modifier = MaterialTheme.colorScheme.replyModifier,
|
||||
isQuotedNote = true,
|
||||
unPackReply = ReplyRenderType.NONE,
|
||||
makeItShort = true,
|
||||
quotesLeft = quotesLeft - 1,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (aboutPubkey != null) {
|
||||
LoadUser(aboutPubkey, accountViewModel) {
|
||||
if (it != null) {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
Text(
|
||||
text = stringRes(R.string.attestation_requests_attestation_to),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
UserCompose(it, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun RenderAttestorRecommendation(
|
||||
note: Note,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteState by note
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
val noteEvent = noteState.note.event as? AttestorRecommendationEvent ?: return
|
||||
|
||||
val kinds = remember(noteEvent) { noteEvent.kinds() }
|
||||
val description = remember(noteEvent) { noteEvent.description() }
|
||||
val aboutPubKey = remember(noteEvent) { noteEvent.dTag() }
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.tertiary.copy(alpha = 0.3f),
|
||||
RoundedCornerShape(12.dp),
|
||||
).background(MaterialTheme.colorScheme.tertiary.copy(alpha = 0.06f))
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Recommend,
|
||||
contentDescription = stringRes(R.string.attestor_recommendation),
|
||||
tint = MaterialTheme.colorScheme.tertiary,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.attestor_recommendation),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
LoadUser(aboutPubKey, accountViewModel) {
|
||||
if (it != null) {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
UserCompose(it, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
}
|
||||
|
||||
if (kinds.isNotEmpty()) {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.attestor_recommendation_for_kinds),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
// Kinds
|
||||
kinds.forEach { kind ->
|
||||
KindChip(kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
description?.let {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = true,
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tags = remember(note) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList },
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
callbackUri = note.toNostrUri(),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun RenderAttestorProficiency(
|
||||
note: Note,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteState by note
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
val noteEvent = noteState.note.event as? AttestorProficiencyEvent ?: return
|
||||
|
||||
val kinds = remember(noteEvent) { noteEvent.kinds() }
|
||||
val description = remember(noteEvent) { noteEvent.description() }
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.secondary.copy(alpha = 0.3f),
|
||||
RoundedCornerShape(12.dp),
|
||||
).background(MaterialTheme.colorScheme.secondary.copy(alpha = 0.06f))
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = stringRes(R.string.attestor_proficiency),
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.attestor_proficiency),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
}
|
||||
|
||||
if (kinds.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
kinds.forEach { kind ->
|
||||
Text(
|
||||
text = "Kind $kind",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(MaterialTheme.colorScheme.secondary.copy(alpha = 0.12f))
|
||||
.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
description?.let {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = true,
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tags = remember(note) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList },
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
callbackUri = note.toNostrUri(),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun attestationColor(
|
||||
status: AttestationStatus?,
|
||||
validity: Validity?,
|
||||
): Color =
|
||||
when {
|
||||
status == AttestationStatus.REVOKED -> Color(0xFFB71C1C)
|
||||
status == AttestationStatus.REJECTED -> Color(0xFFB71C1C)
|
||||
validity == Validity.INVALID -> Color(0xFFB71C1C)
|
||||
status == AttestationStatus.VERIFIED -> Color(0xFF2E7D32)
|
||||
validity == Validity.VALID -> Color(0xFF2E7D32)
|
||||
status == AttestationStatus.VERIFYING -> Color(0xFFF57F17)
|
||||
status == AttestationStatus.ACCEPTED -> Color(0xFF1565C0)
|
||||
else -> Color(0xFF757575)
|
||||
}
|
||||
|
||||
private fun attestationIcon(
|
||||
status: AttestationStatus?,
|
||||
validity: Validity?,
|
||||
): ImageVector =
|
||||
when {
|
||||
status == AttestationStatus.REVOKED -> Icons.Default.Close
|
||||
status == AttestationStatus.REJECTED -> Icons.Default.Close
|
||||
validity == Validity.INVALID -> Icons.Default.Close
|
||||
status == AttestationStatus.VERIFIED -> Icons.Default.VerifiedUser
|
||||
validity == Validity.VALID -> Icons.Default.CheckCircle
|
||||
status == AttestationStatus.VERIFYING -> Icons.Default.HourglassTop
|
||||
status == AttestationStatus.ACCEPTED -> Icons.Default.CheckCircle
|
||||
else -> Icons.Default.VerifiedUser
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun attestationStatusLabel(
|
||||
status: AttestationStatus?,
|
||||
validity: Validity?,
|
||||
): String =
|
||||
when {
|
||||
status == AttestationStatus.REVOKED -> stringRes(R.string.attestation_status_revoked)
|
||||
status == AttestationStatus.REJECTED -> stringRes(R.string.attestation_status_rejected)
|
||||
validity == Validity.INVALID -> stringRes(R.string.attestation_invalid)
|
||||
status == AttestationStatus.VERIFIED -> stringRes(R.string.attestation_status_verified)
|
||||
validity == Validity.VALID -> stringRes(R.string.attestation_valid)
|
||||
status == AttestationStatus.VERIFYING -> stringRes(R.string.attestation_status_verifying)
|
||||
status == AttestationStatus.ACCEPTED -> stringRes(R.string.attestation_status_accepted)
|
||||
else -> stringRes(R.string.attestation)
|
||||
}
|
||||
|
||||
private fun formatTimestamp(timestamp: Long): String {
|
||||
val sdf = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault())
|
||||
return sdf.format(Date(timestamp * 1000))
|
||||
}
|
||||
+83
-107
@@ -21,10 +21,19 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.display
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.BookmarkRemove
|
||||
import androidx.compose.material.icons.outlined.CellTower
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Lock
|
||||
import androidx.compose.material.icons.outlined.LockOpen
|
||||
import androidx.compose.material.icons.outlined.PersonAdd
|
||||
import androidx.compose.material.icons.outlined.PersonRemove
|
||||
import androidx.compose.material.icons.outlined.Report
|
||||
import androidx.compose.material.icons.outlined.Schedule
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -42,6 +51,9 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.EditPostView
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo
|
||||
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
|
||||
@@ -52,7 +64,6 @@ import com.vitorpamplona.amethyst.ui.note.types.EditState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.report.ReportNoteDialog
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -76,6 +87,7 @@ fun BookmarkGroupItemOptions(
|
||||
onClick = { popupExpanded.value = true },
|
||||
) {
|
||||
VerticalDotsIcon()
|
||||
}
|
||||
|
||||
if (popupExpanded.value) {
|
||||
BookmarkGroupItemOptionsMenu(
|
||||
@@ -90,7 +102,6 @@ fun BookmarkGroupItemOptions(
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -142,89 +153,71 @@ fun BookmarkGroupItemOptionsMenu(
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = true,
|
||||
onDismissRequest = onDismiss,
|
||||
M3ActionDialog(
|
||||
title = stringRes(R.string.bookmark_item_actions_dialog_title),
|
||||
onDismiss = onDismiss,
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val actContext = LocalContext.current
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(if (isBookmarkItemPrivate) R.string.move_bookmark_to_public_label else R.string.move_bookmark_to_private_label)) },
|
||||
onClick = if (isBookmarkItemPrivate) onMoveBookmarkToPublic else onMoveBookmarkToPrivate,
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.bookmark_remove_action_label)) },
|
||||
onClick = {
|
||||
|
||||
// Bookmark Management section
|
||||
M3ActionSection {
|
||||
M3ActionRow(
|
||||
icon = if (isBookmarkItemPrivate) Icons.Outlined.LockOpen else Icons.Outlined.Lock,
|
||||
text = stringRes(if (isBookmarkItemPrivate) R.string.move_bookmark_to_public_label else R.string.move_bookmark_to_private_label),
|
||||
) {
|
||||
if (isBookmarkItemPrivate) onMoveBookmarkToPublic() else onMoveBookmarkToPrivate()
|
||||
}
|
||||
M3ActionRow(
|
||||
icon = Icons.Outlined.BookmarkRemove,
|
||||
text = stringRes(R.string.bookmark_remove_action_label),
|
||||
isDestructive = true,
|
||||
) {
|
||||
onDeleteBookmarkItem()
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
|
||||
// Follow section
|
||||
M3ActionSection {
|
||||
if (!state.isFollowingAuthor) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.follow)) },
|
||||
onClick = {
|
||||
val author = note.author ?: return@DropdownMenuItem
|
||||
M3ActionRow(icon = Icons.Outlined.PersonAdd, text = stringRes(R.string.follow)) {
|
||||
val author = note.author ?: return@M3ActionRow
|
||||
accountViewModel.follow(author)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.unfollow)) },
|
||||
onClick = {
|
||||
val author = note.author ?: return@DropdownMenuItem
|
||||
M3ActionRow(icon = Icons.Outlined.PersonRemove, text = stringRes(R.string.unfollow)) {
|
||||
val author = note.author ?: return@M3ActionRow
|
||||
accountViewModel.unfollow(author)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
// DropdownMenuItem(
|
||||
// text = { Text(text = stringRes(R.string.follow_set_add_author_from_note_action)) },
|
||||
// onClick = {
|
||||
// val authorHexKey = note.author?.pubkeyHex ?: return@DropdownMenuItem
|
||||
// nav.nav(Route.PeopleListManagement(authorHexKey))
|
||||
// onDismiss()
|
||||
// },
|
||||
// )
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.copy_text)) },
|
||||
onClick = {
|
||||
}
|
||||
}
|
||||
|
||||
// Copy & Share section
|
||||
M3ActionSection {
|
||||
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_text)) {
|
||||
val lastNoteVersion = (editState?.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value ?: note
|
||||
accountViewModel.decrypt(lastNoteVersion) {
|
||||
clipboardManager.setText(AnnotatedString(it))
|
||||
}
|
||||
accountViewModel.decrypt(lastNoteVersion) { clipboardManager.setText(AnnotatedString(it)) }
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.copy_user_pubkey)) },
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_user_pubkey)) {
|
||||
note.author?.let {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(AnnotatedString("nostr:${it.pubkeyNpub()}"))
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.copy_note_id)) },
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_note_id)) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(AnnotatedString(note.toNostrUri()))
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.quick_action_share)) },
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.Share, text = stringRes(R.string.quick_action_share)) {
|
||||
val sendIntent =
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
@@ -243,74 +236,57 @@ fun BookmarkGroupItemOptionsMenu(
|
||||
Intent.createChooser(sendIntent, stringRes(actContext, R.string.quick_action_share))
|
||||
ContextCompat.startActivity(actContext, shareIntent, null)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
|
||||
// Edit & Broadcast section
|
||||
M3ActionSection {
|
||||
if (state.isLoggedUser && note.isDraft()) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.edit_draft)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.edit_draft)) {
|
||||
nav.nav {
|
||||
routeEditDraftTo(note, accountViewModel.account)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if (note.event is TextNoteEvent && !note.isDraft()) {
|
||||
if (state.isLoggedUser) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.edit_post)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.edit_post)) {
|
||||
wantsToEditPost.value = true
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.propose_an_edit)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.propose_an_edit)) {
|
||||
wantsToEditPost.value = true
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.broadcast)) },
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.CellTower, text = stringRes(R.string.broadcast)) {
|
||||
accountViewModel.broadcast(note)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
|
||||
// Timestamp & Moderation section
|
||||
M3ActionSection {
|
||||
if (accountViewModel.account.otsState.hasPendingAttestations(note)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.timestamp_pending)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.Schedule, text = stringRes(R.string.timestamp_pending)) {
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.timestamp_it)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.Schedule, text = stringRes(R.string.timestamp_it)) {
|
||||
accountViewModel.timestamp(note)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
if (state.isLoggedUser) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.request_deletion)) },
|
||||
onClick = {
|
||||
M3ActionRow(icon = Icons.Outlined.Delete, text = stringRes(R.string.request_deletion), isDestructive = true) {
|
||||
accountViewModel.delete(note)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.block_report)) },
|
||||
onClick = { reportDialogShowing = true },
|
||||
)
|
||||
M3ActionRow(icon = Icons.Outlined.Report, text = stringRes(R.string.block_report), isDestructive = true) {
|
||||
reportDialogShowing = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+31
-43
@@ -30,11 +30,11 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CellTower
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -57,6 +57,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar
|
||||
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
@@ -65,7 +68,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -304,6 +306,31 @@ fun BookmarkGroupActionsMenuButton(
|
||||
) {
|
||||
val isActionListOpen = remember { mutableStateOf(false) }
|
||||
|
||||
if (isActionListOpen.value) {
|
||||
M3ActionDialog(
|
||||
title = stringRes(R.string.list_actions_dialog_title),
|
||||
onDismiss = { isActionListOpen.value = false },
|
||||
) {
|
||||
M3ActionSection {
|
||||
M3ActionRow(
|
||||
icon = Icons.Outlined.CellTower,
|
||||
text = stringRes(R.string.bookmark_list_broadcast_btn_label),
|
||||
) {
|
||||
onBroadcastList()
|
||||
isActionListOpen.value = false
|
||||
}
|
||||
M3ActionRow(
|
||||
icon = Icons.Outlined.Delete,
|
||||
text = stringRes(R.string.bookmark_list_delete_btn_label),
|
||||
isDestructive = true,
|
||||
) {
|
||||
onDeleteList()
|
||||
isActionListOpen.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClickableBox(
|
||||
modifier =
|
||||
StdPadding
|
||||
@@ -319,44 +346,5 @@ fun BookmarkGroupActionsMenuButton(
|
||||
onClick = { isActionListOpen.value = true },
|
||||
) {
|
||||
VerticalDotsIcon()
|
||||
BookmarkGroupActionsMenu(
|
||||
onCloseMenu = { isActionListOpen.value = false },
|
||||
isOpen = isActionListOpen.value,
|
||||
onBroadcastList = onBroadcastList,
|
||||
onDeleteList = onDeleteList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BookmarkGroupActionsMenu(
|
||||
onCloseMenu: () -> Unit,
|
||||
isOpen: Boolean,
|
||||
onBroadcastList: () -> Unit,
|
||||
onDeleteList: () -> Unit,
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = isOpen,
|
||||
onDismissRequest = onCloseMenu,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringRes(R.string.bookmark_list_broadcast_btn_label))
|
||||
},
|
||||
onClick = {
|
||||
onBroadcastList()
|
||||
onCloseMenu()
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringRes(R.string.bookmark_list_delete_btn_label))
|
||||
},
|
||||
onClick = {
|
||||
onDeleteList()
|
||||
onCloseMenu()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+26
-34
@@ -32,10 +32,12 @@ import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Article
|
||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Description
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
@@ -57,6 +59,9 @@ import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.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
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
@@ -258,6 +263,7 @@ private fun BookmarkGroupOptionsButton(
|
||||
onClick = { isMenuOpen.value = true },
|
||||
) {
|
||||
VerticalDotsIcon()
|
||||
}
|
||||
|
||||
GroupOptionsMenu(
|
||||
groupName = bookmarkGroupName,
|
||||
@@ -269,7 +275,6 @@ private fun BookmarkGroupOptionsButton(
|
||||
onGroupClone = onGroupCloneCreate,
|
||||
onDelete = onGroupDelete,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -288,45 +293,32 @@ private fun GroupOptionsMenu(
|
||||
val optionalCloneName = remember { mutableStateOf<String?>(null) }
|
||||
val optionalCloneDescription = remember { mutableStateOf<String?>(null) }
|
||||
|
||||
DropdownMenu(
|
||||
expanded = isExpanded,
|
||||
onDismissRequest = onDismiss,
|
||||
if (isExpanded) {
|
||||
M3ActionDialog(
|
||||
title = stringRes(R.string.group_actions_dialog_title),
|
||||
onDismiss = onDismiss,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.follow_set_rename_btn_label))
|
||||
},
|
||||
onClick = {
|
||||
M3ActionSection {
|
||||
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.follow_set_rename_btn_label)) {
|
||||
onGroupRename()
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.follow_set_desc_modify_label))
|
||||
},
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.Description, text = stringRes(R.string.follow_set_desc_modify_label)) {
|
||||
onGroupDescriptionChange()
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.follow_set_copy_action_btn_label))
|
||||
},
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.follow_set_copy_action_btn_label)) {
|
||||
isCopyDialogOpen.value = true
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.quick_action_delete))
|
||||
},
|
||||
onClick = {
|
||||
}
|
||||
}
|
||||
M3ActionSection {
|
||||
M3ActionRow(icon = Icons.Outlined.Delete, text = stringRes(R.string.quick_action_delete), isDestructive = true) {
|
||||
onDelete()
|
||||
},
|
||||
)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isCopyDialogOpen.value) {
|
||||
|
||||
+28
-26
@@ -30,12 +30,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BookmarkAdd
|
||||
import androidx.compose.material.icons.filled.BookmarkRemove
|
||||
import androidx.compose.material.icons.outlined.BookmarkAdd
|
||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||
import androidx.compose.material.icons.outlined.Lock
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import androidx.compose.material.icons.outlined.RemoveCircleOutline
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
@@ -48,6 +47,9 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.BookmarkMembershipStatusAndNumberDisplay
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfHalfVertPadding
|
||||
@@ -158,6 +160,30 @@ fun BookmarkManagementOptions(
|
||||
) {
|
||||
val isBookmarkAddTapped = remember { mutableStateOf(false) }
|
||||
|
||||
if (isBookmarkAddTapped.value) {
|
||||
M3ActionDialog(
|
||||
title = stringRes(R.string.add_bookmark_dialog_title),
|
||||
onDismiss = { isBookmarkAddTapped.value = false },
|
||||
) {
|
||||
M3ActionSection {
|
||||
M3ActionRow(
|
||||
icon = Icons.Outlined.BookmarkAdd,
|
||||
text = stringRes(R.string.public_bookmark_add_action_label),
|
||||
) {
|
||||
onAddBookmark(false)
|
||||
isBookmarkAddTapped.value = false
|
||||
}
|
||||
M3ActionRow(
|
||||
icon = Icons.Outlined.Lock,
|
||||
text = stringRes(R.string.private_bookmark_add_action_label),
|
||||
) {
|
||||
onAddBookmark(true)
|
||||
isBookmarkAddTapped.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
@@ -196,29 +222,5 @@ fun BookmarkManagementOptions(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = isBookmarkAddTapped.value,
|
||||
onDismissRequest = { isBookmarkAddTapped.value = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.public_bookmark_add_action_label))
|
||||
},
|
||||
onClick = {
|
||||
onAddBookmark(false)
|
||||
isBookmarkAddTapped.value = false
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.private_bookmark_add_action_label))
|
||||
},
|
||||
onClick = {
|
||||
onAddBookmark(true)
|
||||
isBookmarkAddTapped.value = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+34
-25
@@ -28,8 +28,9 @@ import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material.icons.outlined.DoneAll
|
||||
import androidx.compose.material.icons.outlined.Groups
|
||||
import androidx.compose.material.icons.outlined.MoveToInbox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -48,6 +49,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.ui.components.zonedDrawerSwipe
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -103,12 +107,13 @@ fun MessagesTabHeader(
|
||||
contentDescription = stringRes(id = R.string.more_options),
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
}
|
||||
|
||||
MessagesTabMenu(
|
||||
moreActionsExpanded,
|
||||
{ moreActionsExpanded = false },
|
||||
onMarkKnownAsRead,
|
||||
onMarkNewAsRead,
|
||||
if (moreActionsExpanded) {
|
||||
MessagesMarkAsReadDialog(
|
||||
onDismiss = { moreActionsExpanded = false },
|
||||
onMarkKnownAsRead = onMarkKnownAsRead,
|
||||
onMarkNewAsRead = onMarkNewAsRead,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -142,34 +147,38 @@ fun MessagesPager(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessagesTabMenu(
|
||||
expanded: Boolean,
|
||||
fun MessagesMarkAsReadDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onMarkKnownAsRead: () -> Unit,
|
||||
onMarkNewAsRead: () -> Unit,
|
||||
) {
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.mark_all_known_as_read)) },
|
||||
onClick = {
|
||||
M3ActionDialog(
|
||||
title = stringRes(R.string.mark_as_read_dialog_title),
|
||||
onDismiss = onDismiss,
|
||||
) {
|
||||
M3ActionSection {
|
||||
M3ActionRow(
|
||||
icon = Icons.Outlined.Groups,
|
||||
text = stringRes(R.string.mark_all_known_as_read),
|
||||
) {
|
||||
onMarkKnownAsRead()
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.mark_all_new_as_read)) },
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(
|
||||
icon = Icons.Outlined.MoveToInbox,
|
||||
text = stringRes(R.string.mark_all_new_as_read),
|
||||
) {
|
||||
onMarkNewAsRead()
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.mark_all_as_read)) },
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(
|
||||
icon = Icons.Outlined.DoneAll,
|
||||
text = stringRes(R.string.mark_all_as_read),
|
||||
) {
|
||||
onMarkKnownAsRead()
|
||||
onMarkNewAsRead()
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
@@ -42,6 +43,7 @@ val CommunityPostKinds =
|
||||
ClassifiedsEvent.KIND,
|
||||
LongTextNoteEvent.KIND,
|
||||
CommunityPostApprovalEvent.KIND,
|
||||
AttestationEvent.KIND,
|
||||
)
|
||||
|
||||
fun filterCommunityPosts(
|
||||
|
||||
+10
-1
@@ -29,6 +29,10 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthors
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||
@@ -60,6 +64,7 @@ class HomeNewThreadFeedFilter(
|
||||
LongTextNoteEvent.KIND,
|
||||
LiveChessGameChallengeEvent.KIND,
|
||||
LiveChessGameEndEvent.KIND,
|
||||
AttestationEvent.KIND,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -126,7 +131,11 @@ class HomeNewThreadFeedFilter(
|
||||
noteEvent is AudioHeaderEvent ||
|
||||
noteEvent is ChessGameEvent ||
|
||||
noteEvent is LiveChessGameChallengeEvent ||
|
||||
noteEvent is LiveChessGameEndEvent
|
||||
noteEvent is LiveChessGameEndEvent ||
|
||||
noteEvent is AttestationEvent ||
|
||||
noteEvent is AttestationRequestEvent ||
|
||||
noteEvent is AttestorRecommendationEvent ||
|
||||
noteEvent is AttestorProficiencyEvent
|
||||
) &&
|
||||
filterParams.match(noteEvent, it.relays) &&
|
||||
it.isNewThread()
|
||||
|
||||
+3
-3
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.filterHomePostsByScopes
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
@@ -36,7 +37,6 @@ import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||
|
||||
val HomePostsByGeohashKinds =
|
||||
listOf(
|
||||
@@ -46,11 +46,11 @@ val HomePostsByGeohashKinds =
|
||||
LongTextNoteEvent.KIND,
|
||||
ClassifiedsEvent.KIND,
|
||||
HighlightEvent.KIND,
|
||||
InteractiveStoryPrologueEvent.KIND,
|
||||
WikiNoteEvent.KIND,
|
||||
CommentEvent.KIND,
|
||||
VoiceEvent.KIND,
|
||||
VoiceReplyEvent.KIND,
|
||||
AttestationEvent.KIND,
|
||||
InteractiveStoryPrologueEvent.KIND,
|
||||
)
|
||||
|
||||
fun filterHomePostsByGeohashes(
|
||||
|
||||
+12
-2
@@ -23,7 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.HomePostsConversationKinds
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.HomePostsNewThreadKinds
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.HomePostsNewThreadKinds1
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.HomePostsNewThreadKinds2
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
|
||||
@@ -43,11 +44,20 @@ fun filterHomePostsByGlobal(
|
||||
relay = relayUrl,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = HomePostsNewThreadKinds,
|
||||
kinds = HomePostsNewThreadKinds1,
|
||||
limit = 50,
|
||||
since = since ?: newThreadSince,
|
||||
),
|
||||
),
|
||||
RelayBasedFilter(
|
||||
relay = relayUrl,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = HomePostsNewThreadKinds2,
|
||||
limit = 5,
|
||||
since = since ?: newThreadSince,
|
||||
),
|
||||
),
|
||||
RelayBasedFilter(
|
||||
relay = relayUrl,
|
||||
filter =
|
||||
|
||||
+3
-3
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.filterHomePostsByScopes
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
||||
@@ -39,7 +40,6 @@ import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||
|
||||
val HomePostsBuHashtagsKinds =
|
||||
listOf(
|
||||
@@ -50,12 +50,12 @@ val HomePostsBuHashtagsKinds =
|
||||
ClassifiedsEvent.KIND,
|
||||
HighlightEvent.KIND,
|
||||
AudioHeaderEvent.KIND,
|
||||
InteractiveStoryPrologueEvent.KIND,
|
||||
CommentEvent.KIND,
|
||||
WikiNoteEvent.KIND,
|
||||
NipTextEvent.KIND,
|
||||
VoiceEvent.KIND,
|
||||
VoiceReplyEvent.KIND,
|
||||
AttestationEvent.KIND,
|
||||
InteractiveStoryPrologueEvent.KIND,
|
||||
)
|
||||
|
||||
fun filterHomePostsByHashtags(
|
||||
|
||||
+20
-4
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follo
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
||||
@@ -50,7 +51,7 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||
import kotlin.math.min
|
||||
|
||||
val HomePostsNewThreadKinds =
|
||||
val HomePostsNewThreadKinds1 =
|
||||
listOf(
|
||||
TextNoteEvent.KIND,
|
||||
RepostEvent.KIND,
|
||||
@@ -58,10 +59,15 @@ val HomePostsNewThreadKinds =
|
||||
ClassifiedsEvent.KIND,
|
||||
LongTextNoteEvent.KIND,
|
||||
HighlightEvent.KIND,
|
||||
WikiNoteEvent.KIND,
|
||||
NipTextEvent.KIND,
|
||||
ZapPollEvent.KIND,
|
||||
PollEvent.KIND,
|
||||
WikiNoteEvent.KIND,
|
||||
AttestationEvent.KIND,
|
||||
NipTextEvent.KIND,
|
||||
)
|
||||
|
||||
val HomePostsNewThreadKinds2 =
|
||||
listOf(
|
||||
InteractiveStoryPrologueEvent.KIND,
|
||||
ChessGameEvent.KIND,
|
||||
LiveChessGameChallengeEvent.KIND,
|
||||
@@ -91,12 +97,22 @@ fun filterNewHomePostsByAuthors(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = HomePostsNewThreadKinds,
|
||||
kinds = HomePostsNewThreadKinds1,
|
||||
authors = authorList,
|
||||
limit = min(authorList.size * 10, 500),
|
||||
since = since,
|
||||
),
|
||||
),
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = HomePostsNewThreadKinds2,
|
||||
authors = authorList,
|
||||
limit = min(authorList.size * 10, 5),
|
||||
since = since,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+26
-35
@@ -38,11 +38,14 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CellTower
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults.cardElevation
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -75,6 +78,9 @@ import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
@@ -416,15 +422,16 @@ private fun ListActionsMenuButton(
|
||||
onClick = { isActionListOpen.value = true },
|
||||
) {
|
||||
VerticalDotsIcon()
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = isActionListOpen.value,
|
||||
onDismissRequest = { isActionListOpen.value = false },
|
||||
) {
|
||||
if (isActionListOpen.value) {
|
||||
val context = LocalContext.current
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.quick_action_share)) },
|
||||
onClick = {
|
||||
M3ActionDialog(
|
||||
title = stringRes(R.string.list_actions_dialog_title),
|
||||
onDismiss = { isActionListOpen.value = false },
|
||||
) {
|
||||
M3ActionSection {
|
||||
M3ActionRow(icon = Icons.Outlined.Share, text = stringRes(R.string.quick_action_share)) {
|
||||
val sendIntent =
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
@@ -443,38 +450,22 @@ private fun ListActionsMenuButton(
|
||||
Intent.createChooser(sendIntent, stringRes(context, R.string.quick_action_share))
|
||||
ContextCompat.startActivity(context, shareIntent, null)
|
||||
isActionListOpen.value = false
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringRes(R.string.follow_set_edit_list_metadata))
|
||||
},
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.follow_set_edit_list_metadata)) {
|
||||
onEditList()
|
||||
isActionListOpen.value = false
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringRes(R.string.follow_set_broadcast))
|
||||
},
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.CellTower, text = stringRes(R.string.follow_set_broadcast)) {
|
||||
onBroadcastList()
|
||||
isActionListOpen.value = false
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringRes(R.string.follow_set_delete))
|
||||
},
|
||||
onClick = {
|
||||
}
|
||||
}
|
||||
M3ActionSection {
|
||||
M3ActionRow(icon = Icons.Outlined.Delete, text = stringRes(R.string.follow_set_delete), isDestructive = true) {
|
||||
onDeleteList()
|
||||
isActionListOpen.value = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
-35
@@ -35,11 +35,14 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CellTower
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults.cardElevation
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -68,6 +71,9 @@ import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
@@ -271,15 +277,16 @@ private fun ListActionsMenuButton(
|
||||
onClick = { isActionListOpen.value = true },
|
||||
) {
|
||||
VerticalDotsIcon()
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = isActionListOpen.value,
|
||||
onDismissRequest = { isActionListOpen.value = false },
|
||||
) {
|
||||
if (isActionListOpen.value) {
|
||||
val context = LocalContext.current
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.quick_action_share)) },
|
||||
onClick = {
|
||||
M3ActionDialog(
|
||||
title = stringRes(R.string.pack_actions_dialog_title),
|
||||
onDismiss = { isActionListOpen.value = false },
|
||||
) {
|
||||
M3ActionSection {
|
||||
M3ActionRow(icon = Icons.Outlined.Share, text = stringRes(R.string.quick_action_share)) {
|
||||
val sendIntent =
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
@@ -298,38 +305,22 @@ private fun ListActionsMenuButton(
|
||||
Intent.createChooser(sendIntent, stringRes(context, R.string.quick_action_share))
|
||||
ContextCompat.startActivity(context, shareIntent, null)
|
||||
isActionListOpen.value = false
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringRes(R.string.follow_pack_edit_list_metadata))
|
||||
},
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.follow_pack_edit_list_metadata)) {
|
||||
onEditList()
|
||||
isActionListOpen.value = false
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringRes(R.string.follow_pack_broadcast))
|
||||
},
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.CellTower, text = stringRes(R.string.follow_pack_broadcast)) {
|
||||
onBroadcastList()
|
||||
isActionListOpen.value = false
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringRes(R.string.follow_pack_delete))
|
||||
},
|
||||
onClick = {
|
||||
}
|
||||
}
|
||||
M3ActionSection {
|
||||
M3ActionRow(icon = Icons.Outlined.Delete, text = stringRes(R.string.follow_pack_delete), isDestructive = true) {
|
||||
onDeleteList()
|
||||
isActionListOpen.value = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-27
@@ -29,13 +29,14 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Groups
|
||||
import androidx.compose.material.icons.outlined.Lock
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.Text
|
||||
@@ -59,6 +60,9 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
@@ -286,6 +290,7 @@ private fun PeopleListOptionsButton(
|
||||
onClick = { isMenuOpen.value = true },
|
||||
) {
|
||||
VerticalDotsIcon()
|
||||
}
|
||||
|
||||
ListOptionsMenu(
|
||||
isExpanded = isMenuOpen.value,
|
||||
@@ -294,7 +299,6 @@ private fun PeopleListOptionsButton(
|
||||
onDelete = onListDelete,
|
||||
onDismiss = { isMenuOpen.value = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -309,36 +313,28 @@ private fun ListOptionsMenu(
|
||||
val optionalCloneName = remember { mutableStateOf<String?>(null) }
|
||||
val optionalCloneDescription = remember { mutableStateOf<String?>(null) }
|
||||
|
||||
DropdownMenu(
|
||||
expanded = isExpanded,
|
||||
onDismissRequest = onDismiss,
|
||||
if (isExpanded) {
|
||||
M3ActionDialog(
|
||||
title = stringRes(R.string.list_management_dialog_title),
|
||||
onDismiss = onDismiss,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.follow_set_edit_list_metadata))
|
||||
},
|
||||
onClick = {
|
||||
M3ActionSection {
|
||||
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.follow_set_edit_list_metadata)) {
|
||||
onListEditMetadata()
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.follow_set_copy_action_btn_label))
|
||||
},
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.follow_set_copy_action_btn_label)) {
|
||||
isCopyDialogOpen.value = true
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.quick_action_delete))
|
||||
},
|
||||
onClick = {
|
||||
}
|
||||
}
|
||||
M3ActionSection {
|
||||
M3ActionRow(icon = Icons.Outlined.Delete, text = stringRes(R.string.quick_action_delete), isDestructive = true) {
|
||||
onDelete()
|
||||
},
|
||||
)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isCopyDialogOpen.value) {
|
||||
|
||||
+28
-26
@@ -33,10 +33,9 @@ import androidx.compose.material.icons.filled.PersonAdd
|
||||
import androidx.compose.material.icons.filled.PersonRemove
|
||||
import androidx.compose.material.icons.outlined.Groups
|
||||
import androidx.compose.material.icons.outlined.Lock
|
||||
import androidx.compose.material.icons.outlined.PersonAdd
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import androidx.compose.material.icons.outlined.RemoveCircleOutline
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
@@ -50,6 +49,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.DisplayParticipantNumberAndStatus
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfHalfVertPadding
|
||||
@@ -193,6 +195,30 @@ private fun UserAdditionOptions(
|
||||
) {
|
||||
val isUserAddTapped = remember { mutableStateOf(false) }
|
||||
|
||||
if (isUserAddTapped.value) {
|
||||
M3ActionDialog(
|
||||
title = stringRes(R.string.add_member_dialog_title),
|
||||
onDismiss = { isUserAddTapped.value = false },
|
||||
) {
|
||||
M3ActionSection {
|
||||
M3ActionRow(
|
||||
icon = Icons.Outlined.PersonAdd,
|
||||
text = stringRes(R.string.follow_set_public_member_add_label),
|
||||
) {
|
||||
onAddUserToList(false)
|
||||
isUserAddTapped.value = false
|
||||
}
|
||||
M3ActionRow(
|
||||
icon = Icons.Outlined.Lock,
|
||||
text = stringRes(R.string.follow_set_private_member_add_label),
|
||||
) {
|
||||
onAddUserToList(true)
|
||||
isUserAddTapped.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
@@ -231,29 +257,5 @@ private fun UserAdditionOptions(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = isUserAddTapped.value,
|
||||
onDismissRequest = { isUserAddTapped.value = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.follow_set_public_member_add_label))
|
||||
},
|
||||
onClick = {
|
||||
onAddUserToList(false)
|
||||
isUserAddTapped.value = false
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringRes(R.string.follow_set_private_member_add_label))
|
||||
},
|
||||
onClick = {
|
||||
onAddUserToList(true)
|
||||
isUserAddTapped.value = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
@@ -28,11 +28,14 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -64,7 +67,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.donations.Sho
|
||||
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.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
|
||||
@Composable
|
||||
fun RefreshableCardView(
|
||||
@@ -165,6 +170,7 @@ private fun FeedLoaded(
|
||||
nav: INav,
|
||||
) {
|
||||
val items by loaded.feed.collectAsStateWithLifecycle()
|
||||
val openPolls by openPollsState(accountViewModel)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@@ -175,6 +181,37 @@ private fun FeedLoaded(
|
||||
ShowDonationCard(accountViewModel, nav)
|
||||
}
|
||||
|
||||
if (openPolls.isNotEmpty()) {
|
||||
itemsIndexed(
|
||||
items = openPolls,
|
||||
key = { _, item -> "open-poll-${item.idHex}" },
|
||||
contentType = { _, _ -> "OpenPoll" },
|
||||
) { _, note ->
|
||||
Row(modifier = Modifier.padding(start = Size10dp, end = Size10dp, bottom = Size10dp)) {
|
||||
Card(
|
||||
modifier = MaterialTheme.colorScheme.imageModifier,
|
||||
) {
|
||||
OpenPollsSectionHeader()
|
||||
Row(Modifier.fillMaxWidth().animateItem()) {
|
||||
NoteCompose(
|
||||
baseNote = note,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
routeForLastRead = routeForLastRead,
|
||||
isBoostedNote = false,
|
||||
isQuotedNote = false,
|
||||
quotesLeft = 3,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
items = items.list,
|
||||
key = { _, item -> item.id() },
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.notifications
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
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
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@Composable
|
||||
fun openPollsState(accountViewModel: AccountViewModel) =
|
||||
remember(accountViewModel) {
|
||||
val userPubkeyHex = accountViewModel.account.userProfile().pubkeyHex
|
||||
|
||||
val filter =
|
||||
Filter(
|
||||
kinds = listOf(PollEvent.KIND, ZapPollEvent.KIND),
|
||||
authors = listOf(userPubkeyHex),
|
||||
)
|
||||
|
||||
accountViewModel.account.cache
|
||||
.observeNotes(filter)
|
||||
.map { notes -> filterOpenPolls(notes) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
}.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
|
||||
private fun filterOpenPolls(notes: List<Note>): List<Note> {
|
||||
val now = TimeUtils.now()
|
||||
val oneDayInSeconds = TimeUtils.ONE_DAY
|
||||
|
||||
return notes
|
||||
.filter { note ->
|
||||
val event = note.event ?: return@filter false
|
||||
|
||||
when (event) {
|
||||
is PollEvent -> {
|
||||
val endsAt = event.endsAt()
|
||||
if (endsAt != null) endsAt > now else event.createdAt + oneDayInSeconds > now
|
||||
}
|
||||
|
||||
is ZapPollEvent -> {
|
||||
val closedAt = event.closedAt()
|
||||
if (closedAt != null) closedAt > now else event.createdAt + oneDayInSeconds > now
|
||||
}
|
||||
|
||||
else -> {
|
||||
false
|
||||
}
|
||||
}
|
||||
}.sortedByDescending { it.createdAt() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OpenPollsSectionHeader() {
|
||||
Text(
|
||||
text = stringRes(R.string.open_poll),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
+2
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.model.filterIntoSet
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.forks.IForkableEvent
|
||||
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
||||
@@ -87,6 +88,7 @@ class NotificationFeedFilter(
|
||||
VideoVerticalEvent.KIND,
|
||||
VideoHorizontalEvent.KIND,
|
||||
WikiNoteEvent.KIND,
|
||||
AttestationRequestEvent.KIND,
|
||||
)
|
||||
|
||||
val NOTIFICATION_KINDS =
|
||||
|
||||
+2
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
||||
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
|
||||
@@ -66,6 +67,7 @@ val UserProfilePostKinds2 =
|
||||
VoiceReplyEvent.KIND,
|
||||
ZapPollEvent.KIND,
|
||||
PinListEvent.KIND,
|
||||
AttestationEvent.KIND,
|
||||
)
|
||||
|
||||
fun filterUserProfilePosts(
|
||||
|
||||
+61
-71
@@ -21,20 +21,24 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Block
|
||||
import androidx.compose.material.icons.outlined.CheckCircle
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material.icons.outlined.Report
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.ui.note.externalLinkForUser
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
|
||||
@Composable
|
||||
@@ -44,108 +48,94 @@ fun UserProfileDropDownMenu(
|
||||
onDismiss: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = popupExpanded,
|
||||
onDismissRequest = onDismiss,
|
||||
if (!popupExpanded) return
|
||||
|
||||
M3ActionDialog(
|
||||
title = stringRes(R.string.profile_actions_dialog_title),
|
||||
onDismiss = onDismiss,
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.copy_user_id)) },
|
||||
onClick = {
|
||||
clipboardManager.setText(AnnotatedString(user.pubkeyNpub()))
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.quick_action_share)) },
|
||||
onClick = {
|
||||
// Share section
|
||||
M3ActionSection {
|
||||
M3ActionRow(
|
||||
icon = Icons.Outlined.ContentCopy,
|
||||
text = stringRes(R.string.copy_user_id),
|
||||
) {
|
||||
clipboardManager.setText(AnnotatedString(user.pubkeyNpub()))
|
||||
onDismiss()
|
||||
}
|
||||
M3ActionRow(
|
||||
icon = Icons.Outlined.Share,
|
||||
text = stringRes(R.string.quick_action_share),
|
||||
) {
|
||||
val sendIntent =
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = "text/plain"
|
||||
putExtra(
|
||||
Intent.EXTRA_TEXT,
|
||||
externalLinkForUser(user),
|
||||
)
|
||||
putExtra(Intent.EXTRA_TEXT, externalLinkForUser(user))
|
||||
putExtra(
|
||||
Intent.EXTRA_TITLE,
|
||||
stringRes(context, R.string.quick_action_share_browser_link),
|
||||
)
|
||||
}
|
||||
|
||||
val shareIntent =
|
||||
Intent.createChooser(sendIntent, stringRes(context, R.string.quick_action_share))
|
||||
val shareIntent = Intent.createChooser(sendIntent, stringRes(context, R.string.quick_action_share))
|
||||
context.startActivity(shareIntent)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Moderation section (if not self)
|
||||
if (accountViewModel.userProfile() != user) {
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
M3ActionSection {
|
||||
if (accountViewModel.account.isHidden(user)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.unblock_user)) },
|
||||
onClick = {
|
||||
M3ActionRow(
|
||||
icon = Icons.Outlined.CheckCircle,
|
||||
text = stringRes(R.string.unblock_user),
|
||||
) {
|
||||
accountViewModel.show(user)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(id = R.string.block_hide_user)) },
|
||||
onClick = {
|
||||
M3ActionRow(
|
||||
icon = Icons.Outlined.Block,
|
||||
text = stringRes(R.string.block_hide_user),
|
||||
isDestructive = true,
|
||||
) {
|
||||
accountViewModel.hide(user)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(id = R.string.report_spam_scam)) },
|
||||
onClick = {
|
||||
}
|
||||
}
|
||||
|
||||
// Report section
|
||||
M3ActionSection {
|
||||
M3ActionRow(icon = Icons.Outlined.Report, text = stringRes(R.string.report_spam_scam), isDestructive = true) {
|
||||
accountViewModel.report(user, ReportType.SPAM)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.report_hateful_speech)) },
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.Report, text = stringRes(R.string.report_hateful_speech), isDestructive = true) {
|
||||
accountViewModel.report(user, ReportType.PROFANITY)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(id = R.string.report_impersonation)) },
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.Report, text = stringRes(R.string.report_impersonation), isDestructive = true) {
|
||||
accountViewModel.report(user, ReportType.IMPERSONATION)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.report_nudity_porn)) },
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.Report, text = stringRes(R.string.report_nudity_porn), isDestructive = true) {
|
||||
accountViewModel.report(user, ReportType.NUDITY)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(id = R.string.report_illegal_behaviour)) },
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.Report, text = stringRes(R.string.report_illegal_behaviour), isDestructive = true) {
|
||||
accountViewModel.report(user, ReportType.ILLEGAL)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(id = R.string.report_malware)) },
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(icon = Icons.Outlined.Report, text = stringRes(R.string.report_malware), isDestructive = true) {
|
||||
accountViewModel.report(user, ReportType.MALWARE)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -27,6 +27,10 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||
@@ -87,7 +91,11 @@ class UserProfileNewThreadFeedFilter(
|
||||
it.event is AudioTrackEvent ||
|
||||
it.event is AudioHeaderEvent ||
|
||||
it.event is VoiceEvent ||
|
||||
it.event is TorrentEvent
|
||||
it.event is TorrentEvent ||
|
||||
it.event is AttestationEvent ||
|
||||
it.event is AttestationRequestEvent ||
|
||||
it.event is AttestorRecommendationEvent ||
|
||||
it.event is AttestorProficiencyEvent
|
||||
) &&
|
||||
it.isNewThread() &&
|
||||
account.isAcceptable(it)
|
||||
|
||||
+23
-16
@@ -29,8 +29,8 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material.icons.outlined.Description
|
||||
import androidx.compose.material.icons.outlined.FolderZip
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -53,6 +53,9 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.DefaultDMRelayList
|
||||
import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList
|
||||
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -490,6 +493,7 @@ fun SettingsCategoryWithButton(
|
||||
@Composable
|
||||
fun ExportDropdownMenu(collection: () -> RelayListCollection) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val context = LocalContext.current
|
||||
|
||||
IconButton(onClick = { expanded = true }) {
|
||||
Icon(
|
||||
@@ -498,24 +502,27 @@ fun ExportDropdownMenu(collection: () -> RelayListCollection) {
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
if (expanded) {
|
||||
M3ActionDialog(
|
||||
title = stringRes(R.string.export_actions_dialog_title),
|
||||
onDismiss = { expanded = false },
|
||||
) {
|
||||
M3ActionSection {
|
||||
M3ActionRow(
|
||||
icon = Icons.Outlined.Description,
|
||||
text = stringRes(R.string.export_as_text),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.export_as_text)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
RelayExporter(context).export(collection())
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.export_as_zip)) },
|
||||
onClick = {
|
||||
}
|
||||
M3ActionRow(
|
||||
icon = Icons.Outlined.FolderZip,
|
||||
text = stringRes(R.string.export_as_zip),
|
||||
) {
|
||||
expanded = false
|
||||
RelayZipExporter(context).export(collection())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -128,6 +128,10 @@ import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.bitcoinColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.redColorOnSecondSurface
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
|
||||
@@ -529,6 +533,10 @@ private fun kindDisplayName(kind: Int): Int =
|
||||
AppSpecificDataEvent.KIND -> R.string.kind_user_settings
|
||||
AudioHeaderEvent.KIND -> R.string.kind_audio_header
|
||||
AudioTrackEvent.KIND -> R.string.kind_audio_track
|
||||
AttestationEvent.KIND -> R.string.attestation
|
||||
AttestationRequestEvent.KIND -> R.string.attestation_request
|
||||
AttestorRecommendationEvent.KIND -> R.string.attestor_recommendation
|
||||
AttestorProficiencyEvent.KIND -> R.string.attestor_proficiency
|
||||
BadgeAwardEvent.KIND -> R.string.kind_badge_awards
|
||||
BadgeDefinitionEvent.KIND -> R.string.kind_badge_definitions
|
||||
BadgeProfilesEvent.KIND -> R.string.kind_profile_badges
|
||||
@@ -661,7 +669,7 @@ val zaps = setOf(9734, 9735, 9041, 17375, 23194, 23195)
|
||||
val reports = setOf(ReportEvent.KIND, MuteListEvent.KIND, DeletionEvent.KIND, RequestToVanishEvent.KIND)
|
||||
|
||||
@Composable
|
||||
private fun KindChip(kind: Int) {
|
||||
fun KindChip(kind: Int) {
|
||||
val nameResId = kindDisplayName(kind)
|
||||
val name = if (nameResId != -1) stringResource(nameResId) else "k$kind"
|
||||
val (bg, fg) =
|
||||
|
||||
+16
@@ -139,6 +139,10 @@ import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.types.PictureDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAttestation
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAttestationRequest
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAttestorProficiency
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAttestorRecommendation
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarDateSlotEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarTimeSlotEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderChannelMessage
|
||||
@@ -190,6 +194,10 @@ import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.selectedNote
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward
|
||||
@@ -620,6 +628,14 @@ private fun FullBleedNoteCompose(
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
} else if (noteEvent is AttestationEvent) {
|
||||
RenderAttestation(baseNote, 3, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is AttestationRequestEvent) {
|
||||
RenderAttestationRequest(baseNote, 3, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is AttestorRecommendationEvent) {
|
||||
RenderAttestorRecommendation(baseNote, 3, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is AttestorProficiencyEvent) {
|
||||
RenderAttestorProficiency(baseNote, 3, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is AdvertisedRelayListEvent) {
|
||||
DisplayNIP65RelayList(baseNote, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is LnZapEvent) {
|
||||
|
||||
@@ -661,4 +661,5 @@
|
||||
<string name="delete_all">حذف الكل</string>
|
||||
<string name="delete_all_drafts_confirmation">متأكد من حذف جميع المسودات؟</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -573,4 +573,5 @@
|
||||
<string name="donate_now">এখনই দান করুন</string>
|
||||
<string name="accessibility_scan_qr_code">QR কোড স্ক্যান করুন</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1310,6 +1310,10 @@
|
||||
<string name="my_lists">Moje seznamy</string>
|
||||
<string name="people_list_label">Uživatelé</string>
|
||||
<string name="select_list_to_filter">Vyberte seznam pro filtrování kanálu</string>
|
||||
<string name="feed_group_feeds">Kanály</string>
|
||||
<string name="feed_group_hashtags">Hashtagy</string>
|
||||
<string name="feed_group_communities">Komunity</string>
|
||||
<string name="feed_group_lists">Seznamy</string>
|
||||
<string name="temporary_account">Odhlásit se na zámek zařízení</string>
|
||||
<string name="private_message">Soukromá zpráva</string>
|
||||
<string name="public_message">Veřejná zpráva</string>
|
||||
@@ -1621,4 +1625,38 @@
|
||||
<string name="event_sync_status_downloading">Stahování</string>
|
||||
<string name="event_sync_status_error">Chyba</string>
|
||||
<string name="event_sync_status_completed">Dokončeno</string>
|
||||
<!-- M3 Action Dialog titles -->
|
||||
<string name="mark_as_read_dialog_title">Označit jako přečtené</string>
|
||||
<string name="note_actions_dialog_title">Akce poznámky</string>
|
||||
<string name="profile_actions_dialog_title">Akce profilu</string>
|
||||
<string name="media_actions_dialog_title">Akce médií</string>
|
||||
<string name="playback_actions_dialog_title">Přehrávání</string>
|
||||
<string name="pack_actions_dialog_title">Akce balíčku</string>
|
||||
<string name="list_actions_dialog_title">Akce seznamu</string>
|
||||
<string name="bookmark_item_actions_dialog_title">Akce záložky</string>
|
||||
<string name="group_actions_dialog_title">Akce skupiny</string>
|
||||
<string name="add_bookmark_dialog_title">Přidat záložku</string>
|
||||
<string name="add_member_dialog_title">Přidat člena</string>
|
||||
<string name="list_management_dialog_title">Správa seznamu</string>
|
||||
<string name="link_actions_dialog_title">Akce odkazu</string>
|
||||
<string name="export_actions_dialog_title">Exportovat</string>
|
||||
<string name="attestation">Atestace</string>
|
||||
<string name="attestation_valid">Platná</string>
|
||||
<string name="attestation_invalid">Neplatná</string>
|
||||
<string name="attestation_status_accepted">Přijata</string>
|
||||
<string name="attestation_status_rejected">Zamítnuta</string>
|
||||
<string name="attestation_status_verifying">Ověřování</string>
|
||||
<string name="attestation_status_verified">Ověřena</string>
|
||||
<string name="attestation_status_revoked">Odvolána</string>
|
||||
<string name="attestation_valid_from">Platná od %1$s</string>
|
||||
<string name="attestation_valid_to">Platná do %1$s</string>
|
||||
<string name="attestation_request">Žádost o atestaci</string>
|
||||
<string name="attestation_request_description">Žádost o atestaci události</string>
|
||||
<string name="attestor_recommendation">Doporučení atestátora</string>
|
||||
<string name="attestor_recommendation_for_kinds">Doporučen pro druhy: %1$s</string>
|
||||
<string name="attestor_proficiency">Odbornost atestátora</string>
|
||||
<string name="attestor_proficiency_for_kinds">Odborník na ověřování druhů: %1$s</string>
|
||||
<string name="attestation_attests_to">Potvrzuje</string>
|
||||
<string name="attestation_requests_attestation_to">Žádosti o atestaci pro</string>
|
||||
<string name="open_poll">Otevřená anketa</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1315,6 +1315,10 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="my_lists">Meine Listen</string>
|
||||
<string name="people_list_label">Benutzer</string>
|
||||
<string name="select_list_to_filter">Liste zum Filtern des Feeds auswählen</string>
|
||||
<string name="feed_group_feeds">Feeds</string>
|
||||
<string name="feed_group_hashtags">Hash-Tags</string>
|
||||
<string name="feed_group_communities">Gemeinschaften</string>
|
||||
<string name="feed_group_lists">Listen</string>
|
||||
<string name="temporary_account">Beim Sperren des Geräts abmelden</string>
|
||||
<string name="private_message">Private Nachricht</string>
|
||||
<string name="public_message">Öffentliche Nachricht</string>
|
||||
@@ -1626,4 +1630,38 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="event_sync_status_downloading">Herunterladen</string>
|
||||
<string name="event_sync_status_error">Fehler</string>
|
||||
<string name="event_sync_status_completed">Abgeschlossen</string>
|
||||
<!-- M3 Action Dialog titles -->
|
||||
<string name="mark_as_read_dialog_title">Als gelesen markieren</string>
|
||||
<string name="note_actions_dialog_title">Notiz-Aktionen</string>
|
||||
<string name="profile_actions_dialog_title">Profilaktionen</string>
|
||||
<string name="media_actions_dialog_title">Medienaktionen</string>
|
||||
<string name="playback_actions_dialog_title">Wiedergabe</string>
|
||||
<string name="pack_actions_dialog_title">Paket-Aktionen</string>
|
||||
<string name="list_actions_dialog_title">Listenaktionen</string>
|
||||
<string name="bookmark_item_actions_dialog_title">Lesezeichen-Aktionen</string>
|
||||
<string name="group_actions_dialog_title">Gruppenaktionen</string>
|
||||
<string name="add_bookmark_dialog_title">Lesezeichen hinzufügen</string>
|
||||
<string name="add_member_dialog_title">Mitglied hinzufügen</string>
|
||||
<string name="list_management_dialog_title">Listenverwaltung</string>
|
||||
<string name="link_actions_dialog_title">Link-Aktionen</string>
|
||||
<string name="export_actions_dialog_title">Exportieren</string>
|
||||
<string name="attestation">Attestierung</string>
|
||||
<string name="attestation_valid">Gültig</string>
|
||||
<string name="attestation_invalid">Ungültig</string>
|
||||
<string name="attestation_status_accepted">Akzeptiert</string>
|
||||
<string name="attestation_status_rejected">Abgelehnt</string>
|
||||
<string name="attestation_status_verifying">Wird verifiziert</string>
|
||||
<string name="attestation_status_verified">Verifiziert</string>
|
||||
<string name="attestation_status_revoked">Widerrufen</string>
|
||||
<string name="attestation_valid_from">Gültig ab %1$s</string>
|
||||
<string name="attestation_valid_to">Gültig bis %1$s</string>
|
||||
<string name="attestation_request">Attestierungsanfrage</string>
|
||||
<string name="attestation_request_description">Attestierung für ein Ereignis anfordern</string>
|
||||
<string name="attestor_recommendation">Empfehlung des Attestierers</string>
|
||||
<string name="attestor_recommendation_for_kinds">Empfohlen für Arten: %1$s</string>
|
||||
<string name="attestor_proficiency">Kompetenz des Attestierers</string>
|
||||
<string name="attestor_proficiency_for_kinds">Kompetent für die Verifizierung von Arten: %1$s</string>
|
||||
<string name="attestation_attests_to">Bestätigt</string>
|
||||
<string name="attestation_requests_attestation_to">Beantragt Attestierung für</string>
|
||||
<string name="open_poll">Offene Umfrage</string>
|
||||
</resources>
|
||||
|
||||
@@ -515,4 +515,5 @@
|
||||
Μετά την εγκατάσταση, επιλέξτε την εφαρμογή που θέλετε να χρησιμοποιήσετε από τις Ρυθμίσεις.
|
||||
</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -8,4 +8,5 @@
|
||||
<string name="show_anyway">Show Anyway</string>
|
||||
<string name="post_not_found_short">👀</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -443,4 +443,5 @@
|
||||
<string name="error_dialog_talk_to_user">Mesaĝi la Uzanto</string>
|
||||
<string name="error_dialog_button_ok">Okej</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1183,4 +1183,5 @@
|
||||
<string name="this_message_will_disappear_in_days">Este mensaje desaparecerá en %1$d días</string>
|
||||
<string name="select_signer">Seleccionar firmante</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1159,4 +1159,5 @@
|
||||
<string name="dont_translate_from_description">Los idiomas que se muestran aquí no se traducirán. Selecciona un idioma para eliminarlo y traducirlo de nuevo.</string>
|
||||
<string name="select_signer">Seleccionar firmante</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1160,4 +1160,5 @@
|
||||
<string name="dont_translate_from_description">Los idiomas que se muestran aquí no se traducirán. Selecciona un idioma para eliminarlo y traducirlo de nuevo.</string>
|
||||
<string name="select_signer">Seleccionar firmante</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -948,4 +948,5 @@
|
||||
<string name="temporary_account">با قفل کردن دستگاه از حساب کاربری خارج شو</string>
|
||||
<string name="private_message">پیام خصوصی</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -570,4 +570,5 @@
|
||||
<string name="could_not_download_from_the_server">Mediaa ei voitu ladata palvelimelta</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Paikallista tiedostoa ei voitu valmistella ladattavaksi: %1$s</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1365,4 +1365,5 @@
|
||||
<string name="continue_txt">Continuer</string>
|
||||
<string name="skip_for_now">Ignorer pour le moment</string>
|
||||
<string name="select_all">Tout sélectionner</string>
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1558,4 +1558,5 @@
|
||||
<string name="uptime">%1$d%% समय निरन्तर उपलब्ध</string>
|
||||
<string name="namecoin_settings">नामरूप्य स्थापना विकल्प</string>
|
||||
<string name="ots_explorer_settings">द्व्यंकरूप्य समन्वेषक (ओटीएस॰)</string>
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1630,4 +1630,5 @@
|
||||
<string name="event_sync_status_downloading">Letöltés</string>
|
||||
<string name="event_sync_status_error">Hiba</string>
|
||||
<string name="event_sync_status_completed">Kész</string>
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -512,4 +512,5 @@ Seharusnya %3$s</string>
|
||||
Setelah diinstall, Pilih aplikasi yang ingin digunakan di pengaturan.
|
||||
</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -433,4 +433,5 @@
|
||||
<string name="active_for_global">Globale</string>
|
||||
<string name="active_for_search">Cerca</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -390,4 +390,5 @@
|
||||
<string name="automatically_show_url_preview">自動的にURLプレビューを表示</string>
|
||||
<string name="load_image">画像読み込み</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -172,4 +172,5 @@
|
||||
<string name="torrent_download">Lejupielādēt</string>
|
||||
<string name="torrent_no_apps">Nav uzstādītas torrent lietotnes, kas atvērtu un lejupielādētu datni.</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1145,4 +1145,5 @@
|
||||
<string name="follow_set_delete">Verwijder lijst</string>
|
||||
<string name="follow_pack_delete">Verwijder pakket</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1589,4 +1589,5 @@
|
||||
<string name="event_sync_status_downloading">Pobieranie</string>
|
||||
<string name="event_sync_status_error">Błąd</string>
|
||||
<string name="event_sync_status_completed">Zakończone</string>
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1310,6 +1310,10 @@
|
||||
<string name="my_lists">Minhas listas</string>
|
||||
<string name="people_list_label">Usuários</string>
|
||||
<string name="select_list_to_filter">Selecione uma lista para filtrar o feed</string>
|
||||
<string name="feed_group_feeds">Feeds</string>
|
||||
<string name="feed_group_hashtags">Hashtags</string>
|
||||
<string name="feed_group_communities">Comunidades</string>
|
||||
<string name="feed_group_lists">Listas</string>
|
||||
<string name="temporary_account">Terminar sessão no bloqueio do dispositivo</string>
|
||||
<string name="private_message">Mensagem Privada</string>
|
||||
<string name="public_message">Mensagem pública</string>
|
||||
@@ -1621,4 +1625,38 @@
|
||||
<string name="event_sync_status_downloading">Baixando</string>
|
||||
<string name="event_sync_status_error">Erro</string>
|
||||
<string name="event_sync_status_completed">Concluído</string>
|
||||
<!-- M3 Action Dialog titles -->
|
||||
<string name="mark_as_read_dialog_title">Marcar como lida</string>
|
||||
<string name="note_actions_dialog_title">Ações da nota</string>
|
||||
<string name="profile_actions_dialog_title">Ações do perfil</string>
|
||||
<string name="media_actions_dialog_title">Ações de mídia</string>
|
||||
<string name="playback_actions_dialog_title">Reprodução</string>
|
||||
<string name="pack_actions_dialog_title">Ações do pacote</string>
|
||||
<string name="list_actions_dialog_title">Ações da lista</string>
|
||||
<string name="bookmark_item_actions_dialog_title">Ações de favorito</string>
|
||||
<string name="group_actions_dialog_title">Ações do grupo</string>
|
||||
<string name="add_bookmark_dialog_title">Adicionar favorito</string>
|
||||
<string name="add_member_dialog_title">Adicionar membro</string>
|
||||
<string name="list_management_dialog_title">Gerenciamento de lista</string>
|
||||
<string name="link_actions_dialog_title">Ações do link</string>
|
||||
<string name="export_actions_dialog_title">Exportar</string>
|
||||
<string name="attestation">Atestação</string>
|
||||
<string name="attestation_valid">Válida</string>
|
||||
<string name="attestation_invalid">Inválida</string>
|
||||
<string name="attestation_status_accepted">Aceita</string>
|
||||
<string name="attestation_status_rejected">Rejeitada</string>
|
||||
<string name="attestation_status_verifying">Verificando</string>
|
||||
<string name="attestation_status_verified">Verificada</string>
|
||||
<string name="attestation_status_revoked">Revogada</string>
|
||||
<string name="attestation_valid_from">Válida a partir de %1$s</string>
|
||||
<string name="attestation_valid_to">Válida até %1$s</string>
|
||||
<string name="attestation_request">Solicitação de atestação</string>
|
||||
<string name="attestation_request_description">Solicitando atestação para um evento</string>
|
||||
<string name="attestor_recommendation">Recomendação do atestante</string>
|
||||
<string name="attestor_recommendation_for_kinds">Recomendado para tipos: %1$s</string>
|
||||
<string name="attestor_proficiency">Competência do atestante</string>
|
||||
<string name="attestor_proficiency_for_kinds">Competente na verificação de tipos: %1$s</string>
|
||||
<string name="attestation_attests_to">Atesta</string>
|
||||
<string name="attestation_requests_attestation_to">Solicita atestação para</string>
|
||||
<string name="open_poll">Enquete aberta</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -432,4 +432,5 @@
|
||||
<string name="classifieds_category_misc">Разное</string>
|
||||
<string name="classifieds_category_other">Другое</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1642,4 +1642,5 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="event_sync_status_downloading">Prenašam</string>
|
||||
<string name="event_sync_status_error">Napaka</string>
|
||||
<string name="event_sync_status_completed">Zaključeno</string>
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -34,4 +34,5 @@
|
||||
<string name="login_with_a_private_key_to_like_posts">Користите јавни кључ и јавни кључеви су само за читање. Пријавите се са приватним кључем да бисте лајковали постове</string>
|
||||
<string name="no_zap_amount_setup_long_press_to_change">Нема подешавања износа Зап. Дуго притисните да бисте променили</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1309,6 +1309,10 @@
|
||||
<string name="my_lists">Mina listor</string>
|
||||
<string name="people_list_label">Användare</string>
|
||||
<string name="select_list_to_filter">Välj en lista för att filtrera flödet</string>
|
||||
<string name="feed_group_feeds">Flöden</string>
|
||||
<string name="feed_group_hashtags">Hashtaggar</string>
|
||||
<string name="feed_group_communities">Gemenskaper</string>
|
||||
<string name="feed_group_lists">Listor</string>
|
||||
<string name="temporary_account">Logga ut när enheten låses</string>
|
||||
<string name="private_message">Privat meddelande</string>
|
||||
<string name="public_message">Offentligt meddelande</string>
|
||||
@@ -1620,4 +1624,38 @@
|
||||
<string name="event_sync_status_downloading">Laddar ner</string>
|
||||
<string name="event_sync_status_error">Fel</string>
|
||||
<string name="event_sync_status_completed">Slutfört</string>
|
||||
<!-- M3 Action Dialog titles -->
|
||||
<string name="mark_as_read_dialog_title">Markera som läst</string>
|
||||
<string name="note_actions_dialog_title">Anteckningsåtgärder</string>
|
||||
<string name="profile_actions_dialog_title">Profilåtgärder</string>
|
||||
<string name="media_actions_dialog_title">Medieåtgärder</string>
|
||||
<string name="playback_actions_dialog_title">Uppspelning</string>
|
||||
<string name="pack_actions_dialog_title">Paketåtgärder</string>
|
||||
<string name="list_actions_dialog_title">Liståtgärder</string>
|
||||
<string name="bookmark_item_actions_dialog_title">Bokmärkesåtgärder</string>
|
||||
<string name="group_actions_dialog_title">Gruppåtgärder</string>
|
||||
<string name="add_bookmark_dialog_title">Lägg till bokmärke</string>
|
||||
<string name="add_member_dialog_title">Lägg till medlem</string>
|
||||
<string name="list_management_dialog_title">Listhantering</string>
|
||||
<string name="link_actions_dialog_title">Länkåtgärder</string>
|
||||
<string name="export_actions_dialog_title">Exportera</string>
|
||||
<string name="attestation">Attestering</string>
|
||||
<string name="attestation_valid">Giltig</string>
|
||||
<string name="attestation_invalid">Ogiltig</string>
|
||||
<string name="attestation_status_accepted">Accepterad</string>
|
||||
<string name="attestation_status_rejected">Avvisad</string>
|
||||
<string name="attestation_status_verifying">Verifierar</string>
|
||||
<string name="attestation_status_verified">Verifierad</string>
|
||||
<string name="attestation_status_revoked">Återkallad</string>
|
||||
<string name="attestation_valid_from">Giltig från %1$s</string>
|
||||
<string name="attestation_valid_to">Giltig till %1$s</string>
|
||||
<string name="attestation_request">Attesteringsförfrågan</string>
|
||||
<string name="attestation_request_description">Begär attestering för en händelse</string>
|
||||
<string name="attestor_recommendation">Attestantens rekommendation</string>
|
||||
<string name="attestor_recommendation_for_kinds">Rekommenderad för typer: %1$s</string>
|
||||
<string name="attestor_proficiency">Attestantens kompetens</string>
|
||||
<string name="attestor_proficiency_for_kinds">Kompetent att verifiera typer: %1$s</string>
|
||||
<string name="attestation_attests_to">Intygar</string>
|
||||
<string name="attestation_requests_attestation_to">Begär attestering till</string>
|
||||
<string name="open_poll">Öppen omröstning</string>
|
||||
</resources>
|
||||
|
||||
@@ -444,4 +444,5 @@
|
||||
<string name="active_for_global">Ulimwenguni</string>
|
||||
<string name="active_for_search">Tafuta</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -370,4 +370,5 @@
|
||||
<string name="groups_no_descriptor">இந்த அரட்டைக் குழுவின் விளக்கமும் விதிகளும் இன்னும் சேர்க்கபடவில்லை. உரிமையாளரை அணுகி அவற்றை சேர்க்க கோரவும் </string>
|
||||
<string name="community_no_descriptor">இந்த சமூகத்தின் விளக்கமும் விதிகளும் இன்னும் சேர்க்கபடவில்லை. உரிமையாளரை அணுகி அவற்றை சேர்க்க கோரவும்</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -815,4 +815,5 @@
|
||||
<string name="select_list_to_filter">แลือกลิสต์เพื่อโชว์</string>
|
||||
<string name="temporary_account">ออกจากระบบ</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -168,4 +168,5 @@
|
||||
<string name="mark_all_as_read">Tümünü okundu olarak işaretle</string>
|
||||
<string name="biometric_error">Hata</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -500,4 +500,5 @@
|
||||
<string name="could_not_download_from_the_server">Не вдалося завантажити завантажені медіафайли з сервера</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Не вдалося підготувати локальний файл для завантаження: %1$s</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -69,4 +69,5 @@
|
||||
<string name="in_channel">"bu kanalda: "</string>
|
||||
<string name="profile_banner">Profil banneri</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -12,4 +12,5 @@
|
||||
<string name="note_to_receiver">Lưu ý cho người nhận</string>
|
||||
<string name="thank_you_so_much">Cảm ơn rất nhiều!</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1626,4 +1626,5 @@
|
||||
<string name="event_sync_status_downloading">下载中</string>
|
||||
<string name="event_sync_status_error">错误</string>
|
||||
<string name="event_sync_status_completed">已完成</string>
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -240,4 +240,5 @@
|
||||
<string name="pledge_amount_in_sats">聰數量</string>
|
||||
<string name="looking_for_event">"“正在查找事件%1$s”"</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -763,4 +763,5 @@
|
||||
<string name="dvm_waiting_to_confirm_payment">正在等待 DVM 確認付款或返回結果</string>
|
||||
<string name="add_a_nip96_server">添加 NIP-96 服務器</string>
|
||||
<!-- Kind display names for relay subscription filter chips -->
|
||||
<!-- M3 Action Dialog titles -->
|
||||
</resources>
|
||||
|
||||
@@ -258,6 +258,7 @@
|
||||
<string name="error_loading_replies">"Error loading replies: "</string>
|
||||
<string name="try_again">Try again</string>
|
||||
<string name="notification_feed_is_empty">No notifications yet.</string>
|
||||
<string name="open_poll">Open Poll</string>
|
||||
<string name="feed_is_empty">Feed is empty.</string>
|
||||
<string name="refresh">Refresh</string>
|
||||
<string name="created">created</string>
|
||||
@@ -1877,4 +1878,38 @@
|
||||
<string name="event_sync_status_downloading">Downloading</string>
|
||||
<string name="event_sync_status_error">Error</string>
|
||||
<string name="event_sync_status_completed">Completed</string>
|
||||
|
||||
<!-- M3 Action Dialog titles -->
|
||||
<string name="mark_as_read_dialog_title">Mark as Read</string>
|
||||
<string name="note_actions_dialog_title">Note Actions</string>
|
||||
<string name="profile_actions_dialog_title">Profile Actions</string>
|
||||
<string name="media_actions_dialog_title">Media Actions</string>
|
||||
<string name="playback_actions_dialog_title">Playback</string>
|
||||
<string name="pack_actions_dialog_title">Pack Actions</string>
|
||||
<string name="list_actions_dialog_title">List Actions</string>
|
||||
<string name="bookmark_item_actions_dialog_title">Bookmark Actions</string>
|
||||
<string name="group_actions_dialog_title">Group Actions</string>
|
||||
<string name="add_bookmark_dialog_title">Add Bookmark</string>
|
||||
<string name="add_member_dialog_title">Add Member</string>
|
||||
<string name="list_management_dialog_title">List Management</string>
|
||||
<string name="link_actions_dialog_title">Link Actions</string>
|
||||
<string name="export_actions_dialog_title">Export</string>
|
||||
<string name="attestation">Attestation</string>
|
||||
<string name="attestation_valid">Valid</string>
|
||||
<string name="attestation_invalid">Invalid</string>
|
||||
<string name="attestation_status_accepted">Accepted</string>
|
||||
<string name="attestation_status_rejected">Rejected</string>
|
||||
<string name="attestation_status_verifying">Verifying</string>
|
||||
<string name="attestation_status_verified">Verified</string>
|
||||
<string name="attestation_status_revoked">Revoked</string>
|
||||
<string name="attestation_valid_from">Valid from %1$s</string>
|
||||
<string name="attestation_valid_to">Valid to %1$s</string>
|
||||
<string name="attestation_request">Attestation Request</string>
|
||||
<string name="attestation_request_description">Requesting attestation for an event</string>
|
||||
<string name="attestor_recommendation">Attestor Recommendation</string>
|
||||
<string name="attestor_recommendation_for_kinds">Recommended for kinds: </string>
|
||||
<string name="attestor_proficiency">Attestor Proficiency</string>
|
||||
<string name="attestor_proficiency_for_kinds">Proficient in verifying kinds: %1$s</string>
|
||||
<string name="attestation_attests_to">Attests to</string>
|
||||
<string name="attestation_requests_attestation_to">Requests attestation to</string>
|
||||
</resources>
|
||||
|
||||
+97
-3
@@ -23,13 +23,26 @@ package com.vitorpamplona.quartz.experimental.attestations.attestation
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.AttestationStatus
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.Validity
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
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
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.serialization.json.JsonNull.content
|
||||
|
||||
@Immutable
|
||||
class AttestationEvent(
|
||||
@@ -39,7 +52,22 @@ class AttestationEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider,
|
||||
AddressHintProvider,
|
||||
PubKeyHintProvider {
|
||||
override fun eventHints(): List<EventIdHint> = tags.mapNotNull(ETag::parseAsHint)
|
||||
|
||||
override fun linkedEventIds(): List<HexKey> = tags.mapNotNull(ETag::parseId)
|
||||
|
||||
override fun addressHints(): List<AddressHint> = tags.mapNotNull(ATag::parseAsHint)
|
||||
|
||||
override fun linkedAddressIds(): List<String> = tags.mapNotNull(ATag::parseAddressId)
|
||||
|
||||
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
|
||||
|
||||
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
|
||||
|
||||
fun validity() = tags.validity()
|
||||
|
||||
fun status() = tags.status()
|
||||
@@ -50,25 +78,91 @@ class AttestationEvent(
|
||||
|
||||
fun request() = tags.request()
|
||||
|
||||
fun requestId() = tags.requestId()
|
||||
|
||||
fun requestAddress() = tags.requestAddress()
|
||||
|
||||
fun isRevoked() = status() == AttestationStatus.REVOKED
|
||||
|
||||
fun assertionAddrId() = tags.firstNotNullOfOrNull(ATag::parseAddressId)
|
||||
|
||||
fun assertionAddress() = tags.firstNotNullOfOrNull(ATag::parseAddress)
|
||||
|
||||
fun assertionATag() = tags.firstNotNullOfOrNull(ATag::parse)
|
||||
|
||||
fun assertionEventId() = tags.firstNotNullOfOrNull(ETag::parseId)
|
||||
|
||||
fun assertionETag() = tags.firstNotNullOfOrNull(ETag::parse)
|
||||
|
||||
fun assertionPubkey() = tags.firstNotNullOfOrNull(PTag::parseKey)
|
||||
|
||||
fun assertionPTag() = tags.firstNotNullOfOrNull(PTag::parse)
|
||||
|
||||
companion object {
|
||||
const val KIND = 31871
|
||||
const val ALT_DESCRIPTION = "Attestation"
|
||||
|
||||
fun build(
|
||||
fun buildEvent(
|
||||
dTagId: String,
|
||||
about: EventHintBundle<Event>,
|
||||
content: String = "",
|
||||
validity: Validity? = null,
|
||||
status: AttestationStatus? = null,
|
||||
validFrom: Long? = null,
|
||||
validTo: Long? = null,
|
||||
requestAddress: String? = null,
|
||||
requestAddress: EventHintBundle<AttestationRequestEvent>? = null,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<AttestationEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, content, createdAt) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
dTag(dTagId)
|
||||
about(about)
|
||||
validity?.let { validity(it) }
|
||||
status?.let { status(it) }
|
||||
validFrom?.let { validFrom(it) }
|
||||
validTo?.let { validTo(it) }
|
||||
requestAddress?.let { request(it) }
|
||||
initializer()
|
||||
}
|
||||
|
||||
fun buildReplaceable(
|
||||
dTagId: String,
|
||||
about: EventHintBundle<BaseReplaceableEvent>,
|
||||
content: String = "",
|
||||
validity: Validity? = null,
|
||||
status: AttestationStatus? = null,
|
||||
validFrom: Long? = null,
|
||||
validTo: Long? = null,
|
||||
requestAddress: EventHintBundle<AttestationRequestEvent>? = null,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<AttestationEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, content, createdAt) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
dTag(dTagId)
|
||||
aboutReplaceable(about)
|
||||
validity?.let { validity(it) }
|
||||
status?.let { status(it) }
|
||||
validFrom?.let { validFrom(it) }
|
||||
validTo?.let { validTo(it) }
|
||||
requestAddress?.let { request(it) }
|
||||
initializer()
|
||||
}
|
||||
|
||||
fun buildAddress(
|
||||
dTagId: String,
|
||||
about: EventHintBundle<BaseAddressableEvent>,
|
||||
content: String = "",
|
||||
validity: Validity? = null,
|
||||
status: AttestationStatus? = null,
|
||||
validFrom: Long? = null,
|
||||
validTo: Long? = null,
|
||||
requestAddress: EventHintBundle<AttestationRequestEvent>? = null,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<AttestationEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, content, createdAt) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
dTag(dTagId)
|
||||
aboutAddressable(about)
|
||||
validity?.let { validity(it) }
|
||||
status?.let { status(it) }
|
||||
validFrom?.let { validFrom(it) }
|
||||
|
||||
+14
-1
@@ -27,7 +27,14 @@ import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.Valid
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.ValidToTag
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.Validity
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.ValidityTag
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
|
||||
fun TagArrayBuilder<AttestationEvent>.validity(validity: Validity) = addUnique(ValidityTag.assemble(validity))
|
||||
|
||||
@@ -37,4 +44,10 @@ fun TagArrayBuilder<AttestationEvent>.validFrom(timestamp: Long) = addUnique(Val
|
||||
|
||||
fun TagArrayBuilder<AttestationEvent>.validTo(timestamp: Long) = addUnique(ValidToTag.assemble(timestamp))
|
||||
|
||||
fun TagArrayBuilder<AttestationEvent>.request(requestAddress: String) = addUnique(RequestTag.assemble(requestAddress))
|
||||
fun TagArrayBuilder<AttestationEvent>.request(request: EventHintBundle<AttestationRequestEvent>) = addUnique(RequestTag.assemble(request.event.address(), request.relay))
|
||||
|
||||
fun TagArrayBuilder<AttestationEvent>.aboutAddressable(request: EventHintBundle<BaseAddressableEvent>) = addUnique(ATag.assemble(request.event.address(), request.relay))
|
||||
|
||||
fun TagArrayBuilder<AttestationEvent>.aboutReplaceable(request: EventHintBundle<BaseReplaceableEvent>) = addUnique(ATag.assemble(request.event.address(), request.relay))
|
||||
|
||||
fun TagArrayBuilder<AttestationEvent>.about(request: EventHintBundle<Event>) = addUnique(ETag.assemble(request.event.id, request.relay, request.event.pubKey))
|
||||
|
||||
+4
@@ -36,3 +36,7 @@ fun TagArray.validFrom() = firstNotNullOfOrNull(ValidFromTag::parse)
|
||||
fun TagArray.validTo() = firstNotNullOfOrNull(ValidToTag::parse)
|
||||
|
||||
fun TagArray.request() = firstNotNullOfOrNull(RequestTag::parse)
|
||||
|
||||
fun TagArray.requestId() = firstNotNullOfOrNull(RequestTag::parseAddressId)
|
||||
|
||||
fun TagArray.requestAddress() = firstNotNullOfOrNull(RequestTag::parseAddress)
|
||||
|
||||
+90
-4
@@ -20,22 +20,108 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.attestations.attestation.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedAddressTag
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class RequestTag {
|
||||
class RequestTag(
|
||||
val address: Address,
|
||||
val relayHint: NormalizedRelayUrl? = null,
|
||||
) {
|
||||
fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag)
|
||||
|
||||
fun toTagArray() = assemble(address, relayHint)
|
||||
|
||||
fun toTagIdOnly() = assemble(address, null)
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "request"
|
||||
|
||||
fun isTag(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && !Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)
|
||||
|
||||
fun parse(tag: Array<String>): String? {
|
||||
fun isTagged(
|
||||
tag: Array<String>,
|
||||
addressId: String,
|
||||
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == addressId
|
||||
|
||||
fun isTagged(
|
||||
tag: Array<String>,
|
||||
address: ApprovedAddressTag,
|
||||
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == address.toTag()
|
||||
|
||||
fun isIn(
|
||||
tag: Array<String>,
|
||||
addressIds: Set<String>,
|
||||
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in addressIds
|
||||
|
||||
fun parse(tag: Array<String>): ApprovedAddressTag? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
|
||||
|
||||
val address = Address.parse(tag[1]) ?: return null
|
||||
val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }
|
||||
return ApprovedAddressTag(address, relayHint)
|
||||
}
|
||||
|
||||
fun parseValidAddress(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
|
||||
return Address.parse(tag[1])?.toValue()
|
||||
}
|
||||
|
||||
fun parseAddress(tag: Array<String>): Address? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
val address = Address.parse(tag[1]) ?: return null
|
||||
ensure(address.kind != CommunityDefinitionEvent.KIND) { return null }
|
||||
return address
|
||||
}
|
||||
|
||||
fun parseAddressId(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(requestAddress: String) = arrayOf(TAG_NAME, requestAddress)
|
||||
fun parseAsHint(tag: Array<String>): AddressHint? {
|
||||
ensure(tag.has(2)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
|
||||
ensure(tag[1].contains(':')) { return null }
|
||||
ensure(tag[2].isNotEmpty()) { return null }
|
||||
|
||||
val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2])
|
||||
ensure(relayHint != null) { return null }
|
||||
|
||||
return AddressHint(tag[1], relayHint)
|
||||
}
|
||||
|
||||
fun assemble(
|
||||
aTagId: HexKey,
|
||||
relay: NormalizedRelayUrl?,
|
||||
) = arrayOfNotNull(TAG_NAME, aTagId, relay?.url)
|
||||
|
||||
fun assemble(
|
||||
address: Address,
|
||||
relay: NormalizedRelayUrl?,
|
||||
) = arrayOfNotNull(TAG_NAME, address.toValue(), relay?.url)
|
||||
|
||||
fun assemble(
|
||||
kind: Int,
|
||||
pubKey: String,
|
||||
dTag: String,
|
||||
relay: NormalizedRelayUrl?,
|
||||
) = assemble(Address.assemble(kind, pubKey, dTag), relay)
|
||||
}
|
||||
}
|
||||
|
||||
+81
-2
@@ -21,13 +21,27 @@
|
||||
package com.vitorpamplona.quartz.experimental.attestations.request
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.attestorPubKeys
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.cashuToken
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
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
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlin.let
|
||||
|
||||
@Immutable
|
||||
class AttestationRequestEvent(
|
||||
@@ -37,15 +51,45 @@ class AttestationRequestEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider,
|
||||
AddressHintProvider,
|
||||
PubKeyHintProvider {
|
||||
override fun eventHints(): List<EventIdHint> = tags.mapNotNull(ETag::parseAsHint)
|
||||
|
||||
override fun linkedEventIds(): List<HexKey> = tags.mapNotNull(ETag::parseId)
|
||||
|
||||
override fun addressHints(): List<AddressHint> = tags.mapNotNull(ATag::parseAsHint)
|
||||
|
||||
override fun linkedAddressIds(): List<String> = tags.mapNotNull(ATag::parseAddressId)
|
||||
|
||||
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
|
||||
|
||||
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
|
||||
|
||||
fun cashuToken() = tags.cashuToken()
|
||||
|
||||
fun assertionAddrId() = tags.firstNotNullOfOrNull(ATag::parseAddressId)
|
||||
|
||||
fun assertionAddress() = tags.firstNotNullOfOrNull(ATag::parseAddress)
|
||||
|
||||
fun assertionATag() = tags.firstNotNullOfOrNull(ATag::parse)
|
||||
|
||||
fun assertionEventId() = tags.firstNotNullOfOrNull(ETag::parseId)
|
||||
|
||||
fun assertionETag() = tags.firstNotNullOfOrNull(ETag::parse)
|
||||
|
||||
fun assertionPubkey() = tags.firstNotNullOfOrNull(PTag::parseKey)
|
||||
|
||||
fun assertionPTag() = tags.firstNotNullOfOrNull(PTag::parse)
|
||||
|
||||
companion object {
|
||||
const val KIND = 31872
|
||||
const val ALT_DESCRIPTION = "Attestation Request"
|
||||
|
||||
fun build(
|
||||
fun buildEvent(
|
||||
dTagId: String,
|
||||
about: EventHintBundle<Event>,
|
||||
content: String = "",
|
||||
attestorPubKeys: List<HexKey> = emptyList(),
|
||||
cashuToken: String? = null,
|
||||
@@ -54,6 +98,41 @@ class AttestationRequestEvent(
|
||||
) = eventTemplate(KIND, content, createdAt) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
dTag(dTagId)
|
||||
about(about)
|
||||
attestorPubKeys(attestorPubKeys)
|
||||
cashuToken?.let { cashuToken(it) }
|
||||
initializer()
|
||||
}
|
||||
|
||||
fun buildReplaceable(
|
||||
dTagId: String,
|
||||
about: EventHintBundle<BaseReplaceableEvent>,
|
||||
content: String = "",
|
||||
attestorPubKeys: List<HexKey> = emptyList(),
|
||||
cashuToken: String? = null,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<AttestationRequestEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, content, createdAt) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
dTag(dTagId)
|
||||
aboutReplaceable(about)
|
||||
attestorPubKeys(attestorPubKeys)
|
||||
cashuToken?.let { cashuToken(it) }
|
||||
initializer()
|
||||
}
|
||||
|
||||
fun buildAddress(
|
||||
dTagId: String,
|
||||
about: EventHintBundle<BaseAddressableEvent>,
|
||||
content: String = "",
|
||||
attestorPubKeys: List<HexKey> = emptyList(),
|
||||
cashuToken: String? = null,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<AttestationRequestEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, content, createdAt) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
dTag(dTagId)
|
||||
aboutAddressable(about)
|
||||
attestorPubKeys(attestorPubKeys)
|
||||
cashuToken?.let { cashuToken(it) }
|
||||
initializer()
|
||||
|
||||
+12
@@ -21,10 +21,22 @@
|
||||
package com.vitorpamplona.quartz.experimental.attestations.request
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.tags.CashuTokenTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
|
||||
fun TagArrayBuilder<AttestationRequestEvent>.attestorPubKeys(pubKeys: List<HexKey>) = addAll(pubKeys.map { PTag.assemble(it, null) })
|
||||
|
||||
fun TagArrayBuilder<AttestationRequestEvent>.cashuToken(token: String) = addUnique(CashuTokenTag.assemble(token))
|
||||
|
||||
fun TagArrayBuilder<AttestationRequestEvent>.aboutAddressable(request: EventHintBundle<BaseAddressableEvent>) = addUnique(ATag.assemble(request.event.address(), request.relay))
|
||||
|
||||
fun TagArrayBuilder<AttestationRequestEvent>.aboutReplaceable(request: EventHintBundle<BaseReplaceableEvent>) = addUnique(ATag.assemble(request.event.address(), request.relay))
|
||||
|
||||
fun TagArrayBuilder<AttestationRequestEvent>.about(request: EventHintBundle<Event>) = addUnique(ETag.assemble(request.event.id, request.relay, request.event.pubKey))
|
||||
|
||||
+10
@@ -26,6 +26,16 @@ import com.vitorpamplona.quartz.nip01Core.store.IEventStore
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
||||
/**
|
||||
* A reactive event store that combines historical data retrieval with live event streaming.
|
||||
*
|
||||
* This class wraps an [IEventStore] to provide real-time updates. When a [query] is executed,
|
||||
* it first replays all matching historical events from the underlying store, signals the
|
||||
* End of Stored Events (EOSE), and then continues to stream matching new events as they
|
||||
* are inserted.
|
||||
*
|
||||
* @property store The underlying persistent storage for events.
|
||||
*/
|
||||
class LiveEventStore(
|
||||
private val store: IEventStore,
|
||||
) {
|
||||
|
||||
+3
-7
@@ -29,14 +29,10 @@ import kotlinx.coroutines.cancel
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* This class manages per-connection subscriptions as coroutines. Each
|
||||
* subscription (REQ) launches a child coroutine that first replays stored
|
||||
* events matching the filters, sends EOSE, and then streams live events.
|
||||
* Closing a subscription (CLOSE) immediately cancels its coroutine.
|
||||
* Represents a Nostr relay server that manages client connections, event storage, and verification.
|
||||
*
|
||||
* The server is transport-agnostic: callers feed incoming JSON via
|
||||
* [RelaySession.receive] and receive outgoing JSON via the [RelaySession.send] callback
|
||||
* provided to [connect]. This allows use with any WebSocket library.
|
||||
* This class acts as the central coordinator for a relay server, handling the lifecycle of [RelaySession]s
|
||||
* and providing access to the underlying event store.
|
||||
*
|
||||
* @param store The [IEventStore] backing this relay.
|
||||
* @param policyBuilder Controls requirements for relay commands.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user