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
|
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 Relay Feeds
|
||||||
- Adds support for NIP-51 favorite 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
|
- Turn video controller creation into a flow to fix playback lifecycle issues
|
||||||
- Adds support for uploading audio
|
- 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
|
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 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 NIP-66 Relay Monitor and discovery support to Quartz
|
||||||
|
|
||||||
Adds support for Namecoin .bit urls to NIP-05
|
Adds support for Attestations (https://attestr.xyz/)
|
||||||
- Adds choice of ElectrumX server to resolve namecoins.
|
|
||||||
|
|
||||||
Adds basic support for Chess with Jester protocol
|
Adds basic support for Chess with Jester protocol
|
||||||
|
|
||||||
Adds NIP-46 Bunker support to Quartz and Amethyst Desktop
|
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
|
Uploads:
|
||||||
|
- Adds support to upload Documents to all new post screens.
|
||||||
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:
|
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
|
- Displays additional information on warning composables
|
||||||
|
|
||||||
Redesigns and reorganizes Setting pages
|
Settings redesign
|
||||||
- Consolidate drawer settings into a single Settings hub screen
|
- Consolidate drawer settings into a single Settings hub screen
|
||||||
- Redesigns Zap Amount and NWC setup screens
|
- Redesigns Zap Amount and NWC setup screens
|
||||||
- Redesigns Custom zap amount 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
|
- Treat multibyte characters as URL terminators in RichTextParser by @npub1k0jrarx8um0lyw3nmysn50539ky4k8p7gfgzgrsvn8d7lccx3d0s38dczd
|
||||||
- Adds a parser for blossom: uris
|
- 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:
|
Relay Management:
|
||||||
- Adds relay search tooltip when adding relays
|
- 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 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
|
- Adds a complete list of event kind names to the subscription card to relay information
|
||||||
- Tracks and displays connection success rate on relay settings
|
- 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
|
Search fixes
|
||||||
- Breaks the search filter into two subscriptions to prioritize Metadata without punishing content.
|
- Breaks the search filter into two subscriptions to prioritize Metadata without punishing content.
|
||||||
@@ -88,11 +107,14 @@ Search fixes
|
|||||||
|
|
||||||
Profiles:
|
Profiles:
|
||||||
- Adds a profile picture upload button when the user has no picture
|
- 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
|
- Groups received zap amounts by sending the user in the profile tab
|
||||||
- Increases the limit of Zap downloads for profiles to 1000
|
- Increases the limit of Zap downloads for profiles to 1000
|
||||||
- Simplifies profile edit screen layout by @npub1aeh2zw4elewy5682lxc6xnlqzjnxksq303gwu2npfaxd49vmde6qcq4nwx
|
- Simplifies profile edit screen layout by @npub1aeh2zw4elewy5682lxc6xnlqzjnxksq303gwu2npfaxd49vmde6qcq4nwx
|
||||||
- Migrates profile galleries to display a thumbnail for videos
|
- Migrates profile galleries to display a thumbnail for videos
|
||||||
- Fixes profile galleries' aspect ratios
|
- 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
|
Bulk Follow onboarding
|
||||||
- Adds screens to search for a user and to copy his/her follow list
|
- 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
|
- Fixes bug on Show More calculations for very long texts without spaces
|
||||||
- Fixing IO Dispatchers and coroutine scopes of choice
|
- Fixing IO Dispatchers and coroutine scopes of choice
|
||||||
- Fixes anySync parallel operation that was returning the first result, not the first positive "any".
|
- 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:
|
AI:
|
||||||
- Add SKILL.md for AI agent customization
|
- 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 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
|
- 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
|
Adds complete support for iOS to Quartz by @npub1a3tx8wcrt789skl6gg7rqwj4wey0j53eesr4z6asd4h4jwrd62jq0wkq4k
|
||||||
- Provide implementation for Rfc3986 on iOS, using the Swift Rfc3986UriBridge.
|
- Provide implementation for Rfc3986 on iOS, using the Swift Rfc3986UriBridge.
|
||||||
- Provide implementation for LargeCache, using a CacheMap
|
- 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 AESGCM
|
||||||
- Provide implementation for DigestInstance
|
- Provide implementation for DigestInstance
|
||||||
- Provide implementation for LibSodium
|
- Provide implementation for LibSodium
|
||||||
- Adds a reqUntilEoseAsFlow extension to the Nostr Client
|
|
||||||
|
|
||||||
Amethyst Desktop by @npub12cfje6nl2nuxplcqfvhg7ljt89fmpj0n0fd24zxsukja5qm9wmtqd7y76c
|
Amethyst Desktop by @npub12cfje6nl2nuxplcqfvhg7ljt89fmpj0n0fd24zxsukja5qm9wmtqd7y76c
|
||||||
- Adds NIP-46 Bunker Login
|
- Adds NIP-46 Bunker Login
|
||||||
@@ -159,6 +189,8 @@ Amethyst Desktop by @npub12cfje6nl2nuxplcqfvhg7ljt89fmpj0n0fd24zxsukja5qm9wmtqd7
|
|||||||
- Adds encrypted DMs (NIP-04/NIP-17)
|
- Adds encrypted DMs (NIP-04/NIP-17)
|
||||||
- Adds proper empty states with EOSE tracking
|
- Adds proper empty states with EOSE tracking
|
||||||
- Adds multi-column deck layout
|
- 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
|
- Clear stored credentials on logout
|
||||||
- Adds bunker heartbeat indicator
|
- Adds bunker heartbeat indicator
|
||||||
- Adds QR-based signer pairing
|
- Adds QR-based signer pairing
|
||||||
@@ -176,6 +208,7 @@ Amethyst Desktop by @npub12cfje6nl2nuxplcqfvhg7ljt89fmpj0n0fd24zxsukja5qm9wmtqd7
|
|||||||
|
|
||||||
Code Quality
|
Code Quality
|
||||||
- Migrates to AGP 9.0
|
- Migrates to AGP 9.0
|
||||||
|
- Adds Amethyst Desktop to CI/CD and Release builds
|
||||||
- Removes the in-app memory counter methods
|
- Removes the in-app memory counter methods
|
||||||
- Refactors the old NIP-05 code on Quartz
|
- Refactors the old NIP-05 code on Quartz
|
||||||
- Migrates contact list management to addressable notes
|
- 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.BundledInsert
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.ui.note.dateFormatter
|
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.header.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
|
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
|
||||||
@@ -619,6 +623,30 @@ object LocalCache : ILocalCache, ICacheProvider {
|
|||||||
wasVerified: Boolean,
|
wasVerified: Boolean,
|
||||||
) = consumeBaseReplaceable(event, relay, wasVerified)
|
) = 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(
|
fun consumeRegularEvent(
|
||||||
event: Event,
|
event: Event,
|
||||||
relay: NormalizedRelayUrl?,
|
relay: NormalizedRelayUrl?,
|
||||||
@@ -3051,6 +3079,10 @@ object LocalCache : ILocalCache, ICacheProvider {
|
|||||||
is AppDefinitionEvent -> consume(event, relay, wasVerified)
|
is AppDefinitionEvent -> consume(event, relay, wasVerified)
|
||||||
is AppRecommendationEvent -> consume(event, relay, wasVerified)
|
is AppRecommendationEvent -> consume(event, relay, wasVerified)
|
||||||
is AppSpecificDataEvent -> 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 AudioHeaderEvent -> consume(event, relay, wasVerified)
|
||||||
is AudioTrackEvent -> consume(event, relay, wasVerified)
|
is AudioTrackEvent -> consume(event, relay, wasVerified)
|
||||||
is BadgeAwardEvent -> 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.PictureInPicture
|
||||||
import androidx.compose.material.icons.filled.SaveAlt
|
import androidx.compose.material.icons.filled.SaveAlt
|
||||||
import androidx.compose.material.icons.filled.Share
|
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.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -45,9 +42,11 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.vitorpamplona.amethyst.R
|
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.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||||
@@ -134,61 +133,41 @@ fun OverflowMenuButton(
|
|||||||
modifier = Size20Modifier,
|
modifier = Size20Modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
if (menuExpanded.value) {
|
||||||
expanded = menuExpanded.value,
|
M3ActionDialog(
|
||||||
onDismissRequest = { menuExpanded.value = false },
|
title = stringRes(R.string.playback_actions_dialog_title),
|
||||||
containerColor = Color.Black.copy(alpha = 0.85f),
|
onDismiss = { menuExpanded.value = false },
|
||||||
) {
|
) {
|
||||||
|
M3ActionSection {
|
||||||
if (showShare) {
|
if (showShare) {
|
||||||
DropdownMenuItem(
|
M3ActionRow(
|
||||||
text = { Text(stringRes(R.string.share_or_save), color = Color.White) },
|
icon = Icons.Default.Share,
|
||||||
onClick = {
|
text = stringRes(R.string.share_or_save),
|
||||||
|
) {
|
||||||
menuExpanded.value = false
|
menuExpanded.value = false
|
||||||
onShareClick()
|
onShareClick()
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Share,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = Color.White,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (showSave) {
|
if (showSave) {
|
||||||
DropdownMenuItem(
|
M3ActionRow(
|
||||||
text = { Text(stringRes(R.string.download_to_phone), color = Color.White) },
|
icon = Icons.Default.SaveAlt,
|
||||||
onClick = {
|
text = stringRes(R.string.download_to_phone),
|
||||||
|
) {
|
||||||
menuExpanded.value = false
|
menuExpanded.value = false
|
||||||
onSaveClick()
|
onSaveClick()
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.SaveAlt,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = Color.White,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (showPip) {
|
if (showPip) {
|
||||||
DropdownMenuItem(
|
M3ActionRow(
|
||||||
text = { Text(stringRes(R.string.picture_in_picture), color = Color.White) },
|
icon = Icons.Default.PictureInPicture,
|
||||||
onClick = {
|
text = stringRes(R.string.picture_in_picture),
|
||||||
|
) {
|
||||||
menuExpanded.value = false
|
menuExpanded.value = false
|
||||||
onPipClick()
|
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
|
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.ephemChat.chat.EphemeralChatEvent
|
||||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
|
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
|
||||||
@@ -85,6 +87,12 @@ val NotificationsPerKeyKinds2 =
|
|||||||
InteractiveStorySceneEvent.KIND,
|
InteractiveStorySceneEvent.KIND,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val NotificationsPerKeyKinds3 =
|
||||||
|
listOf(
|
||||||
|
AttestationRequestEvent.KIND,
|
||||||
|
AttestorRecommendationEvent.KIND,
|
||||||
|
)
|
||||||
|
|
||||||
fun filterSummaryNotificationsToPubkey(
|
fun filterSummaryNotificationsToPubkey(
|
||||||
relay: NormalizedRelayUrl,
|
relay: NormalizedRelayUrl,
|
||||||
pubkey: HexKey?,
|
pubkey: HexKey?,
|
||||||
@@ -130,7 +138,17 @@ fun filterNotificationsToPubkey(
|
|||||||
Filter(
|
Filter(
|
||||||
kinds = NotificationsPerKeyKinds2,
|
kinds = NotificationsPerKeyKinds2,
|
||||||
tags = mapOf("p" to listOf(pubkey)),
|
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,
|
since = since,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -171,7 +189,17 @@ fun filterJustTheLatestNotificationsToPubkeyFromRandomRelays(
|
|||||||
Filter(
|
Filter(
|
||||||
kinds = NotificationsPerKeyKinds2,
|
kinds = NotificationsPerKeyKinds2,
|
||||||
tags = mapOf("p" to listOf(pubkey)),
|
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,
|
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.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
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.experimental.zapPolls.ZapPollEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
@@ -47,6 +48,7 @@ val RepliesAndReactionsToAddressesKinds1 =
|
|||||||
LnZapEvent.KIND,
|
LnZapEvent.KIND,
|
||||||
ZapPollEvent.KIND,
|
ZapPollEvent.KIND,
|
||||||
CommentEvent.KIND,
|
CommentEvent.KIND,
|
||||||
|
AttestationEvent.KIND,
|
||||||
)
|
)
|
||||||
|
|
||||||
val PostsAndChatMessagesToAddresses =
|
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.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
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.edits.TextNoteModificationEvent
|
||||||
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||||
@@ -53,6 +54,7 @@ val RepliesAndReactionsKinds =
|
|||||||
OtsEvent.KIND,
|
OtsEvent.KIND,
|
||||||
TextNoteModificationEvent.KIND,
|
TextNoteModificationEvent.KIND,
|
||||||
CommentEvent.KIND,
|
CommentEvent.KIND,
|
||||||
|
AttestationEvent.KIND,
|
||||||
)
|
)
|
||||||
|
|
||||||
val RepliesAndReactionsKinds2 =
|
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.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material.icons.outlined.ContentCopy
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.graphics.Color
|
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.innerPostModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.previewCardImageModifier
|
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)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun UrlPreviewCard(
|
fun UrlPreviewCard(
|
||||||
@@ -81,13 +59,22 @@ fun UrlPreviewCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (popupExpanded.value) {
|
if (popupExpanded.value) {
|
||||||
CopyToClipboard(
|
val clipboardManager = LocalClipboardManager.current
|
||||||
popupExpanded = popupExpanded,
|
M3ActionDialog(
|
||||||
content = url,
|
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
|
popupExpanded.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
|
|||||||
+32
-49
@@ -32,15 +32,16 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Report
|
import androidx.compose.material.icons.filled.Report
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material.icons.outlined.Collections
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
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.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -771,63 +772,54 @@ fun ShareMediaAction(
|
|||||||
// Track if video is downloading - hoisted here to block menu dismiss during download
|
// Track if video is downloading - hoisted here to block menu dismiss during download
|
||||||
val isDownloadingVideo = remember { mutableStateOf(false) }
|
val isDownloadingVideo = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
DropdownMenu(
|
if (popupExpanded.value) {
|
||||||
expanded = popupExpanded.value,
|
M3ActionDialog(
|
||||||
onDismissRequest = { if (!isDownloadingVideo.value) onDismiss() },
|
title = stringRes(R.string.media_actions_dialog_title),
|
||||||
|
onDismiss = { if (!isDownloadingVideo.value) onDismiss() },
|
||||||
) {
|
) {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
|
||||||
|
// Copy & Gallery section
|
||||||
|
if ((videoUri != null && !videoUri.startsWith("file")) || postNostrUri != null) {
|
||||||
|
M3ActionSection {
|
||||||
if (videoUri != null && !videoUri.startsWith("file")) {
|
if (videoUri != null && !videoUri.startsWith("file")) {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Link, text = stringRes(R.string.copy_url_to_clipboard)) {
|
||||||
text = { Text(stringRes(R.string.copy_url_to_clipboard)) },
|
|
||||||
onClick = {
|
|
||||||
clipboardManager.setText(AnnotatedString(videoUri))
|
clipboardManager.setText(AnnotatedString(videoUri))
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
postNostrUri?.let {
|
postNostrUri?.let {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_the_note_id_to_the_clipboard)) {
|
||||||
text = { Text(stringRes(R.string.copy_the_note_id_to_the_clipboard)) },
|
|
||||||
onClick = {
|
|
||||||
clipboardManager.setText(AnnotatedString(it))
|
clipboardManager.setText(AnnotatedString(it))
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
M3ActionRow(icon = Icons.Outlined.Collections, text = stringRes(R.string.add_media_to_gallery)) {
|
||||||
postNostrUri?.let {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringRes(R.string.add_media_to_gallery)) },
|
|
||||||
onClick = {
|
|
||||||
if (videoUri != null) {
|
if (videoUri != null) {
|
||||||
val n19 = Nip19Parser.uriToRoute(postNostrUri)?.entity as? NEvent
|
val n19 = Nip19Parser.uriToRoute(postNostrUri)?.entity as? NEvent
|
||||||
if (n19 != null) {
|
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)
|
accountViewModel.toastManager.toast(R.string.media_added, R.string.media_added_to_profile_gallery)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Share section
|
||||||
content?.let {
|
content?.let {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
M3ActionSection {
|
||||||
when (content) {
|
when (content) {
|
||||||
is MediaUrlImage -> {
|
is MediaUrlImage -> {
|
||||||
videoUri?.let {
|
videoUri?.let {
|
||||||
if (videoUri.isNotEmpty()) {
|
if (videoUri.isNotEmpty()) {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Share, text = stringRes(R.string.share_image)) {
|
||||||
text = { Text(stringRes(R.string.share_image)) },
|
|
||||||
onClick = {
|
|
||||||
scope.launch { shareImageFile(context, videoUri, mimeType) }
|
scope.launch { shareImageFile(context, videoUri, mimeType) }
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -835,18 +827,11 @@ fun ShareMediaAction(
|
|||||||
is MediaUrlVideo -> {
|
is MediaUrlVideo -> {
|
||||||
videoUri?.let {
|
videoUri?.let {
|
||||||
if (videoUri.isNotEmpty()) {
|
if (videoUri.isNotEmpty()) {
|
||||||
DropdownMenuItem(
|
M3ActionRow(
|
||||||
text = {
|
icon = Icons.Outlined.Share,
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
text = stringRes(R.string.share_video),
|
||||||
Text(stringRes(R.string.share_video))
|
|
||||||
if (isDownloadingVideo.value) {
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
LoadingAnimation(indicatorSize = 16.dp, circleWidth = 2.dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enabled = !isDownloadingVideo.value,
|
enabled = !isDownloadingVideo.value,
|
||||||
onClick = {
|
) {
|
||||||
isDownloadingVideo.value = true
|
isDownloadingVideo.value = true
|
||||||
scope.launch {
|
scope.launch {
|
||||||
shareVideoFile(
|
shareVideoFile(
|
||||||
@@ -865,21 +850,17 @@ fun ShareMediaAction(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is MediaLocalVideo -> {
|
is MediaLocalVideo -> {
|
||||||
content.localFile?.let { localFile ->
|
content.localFile?.let { localFile ->
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Share, text = stringRes(R.string.share_video)) {
|
||||||
text = { Text(stringRes(R.string.share_video)) },
|
|
||||||
onClick = {
|
|
||||||
scope.launch { shareLocalVideoFile(context, localFile, mimeType) }
|
scope.launch { shareLocalVideoFile(context, localFile, mimeType) }
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -888,6 +869,8 @@ fun ShareMediaAction(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun shareImageFile(
|
private suspend fun shareImageFile(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|||||||
@@ -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.FileStorageHeaderDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.PictureDisplay
|
import com.vitorpamplona.amethyst.ui.note.types.PictureDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition
|
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.RenderAudioHeader
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAudioTrack
|
import com.vitorpamplona.amethyst.ui.note.types.RenderAudioTrack
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderBadgeAward
|
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.normalWithTopMarginNoteModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
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.header.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward
|
import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward
|
||||||
@@ -767,6 +775,22 @@ private fun RenderNoteRow(
|
|||||||
RenderAppDefinition(baseNote, accountViewModel, nav)
|
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 -> {
|
is AudioTrackEvent -> {
|
||||||
RenderAudioTrack(baseNote, ContentScale.FillWidth, accountViewModel, nav)
|
RenderAudioTrack(baseNote, ContentScale.FillWidth, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
|||||||
+90
-139
@@ -21,10 +21,22 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.note.elements
|
package com.vitorpamplona.amethyst.ui.note.elements
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material.icons.outlined.Bookmark
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material.icons.outlined.BookmarkAdd
|
||||||
import androidx.compose.material3.Text
|
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.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.State
|
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.actions.EditPostView
|
||||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
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.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo
|
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.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.report.ReportNoteDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.report.ReportNoteDialog
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
|
||||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||||
@@ -78,6 +92,7 @@ fun MoreOptionsButton(
|
|||||||
onClick = { popupExpanded.value = true },
|
onClick = { popupExpanded.value = true },
|
||||||
) {
|
) {
|
||||||
VerticalDotsIcon()
|
VerticalDotsIcon()
|
||||||
|
}
|
||||||
|
|
||||||
if (popupExpanded.value) {
|
if (popupExpanded.value) {
|
||||||
NoteDropDownMenu(
|
NoteDropDownMenu(
|
||||||
@@ -89,7 +104,6 @@ fun MoreOptionsButton(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class DropDownParams(
|
data class DropDownParams(
|
||||||
@@ -146,213 +160,150 @@ fun NoteDropDownMenu(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
M3ActionDialog(
|
||||||
expanded = true,
|
title = stringRes(R.string.note_actions_dialog_title),
|
||||||
onDismissRequest = onDismiss,
|
onDismiss = onDismiss,
|
||||||
) {
|
) {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
val actContext = LocalContext.current
|
val actContext = LocalContext.current
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// Follow section
|
||||||
|
M3ActionSection {
|
||||||
if (!state.isFollowingAuthor) {
|
if (!state.isFollowingAuthor) {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.PersonAdd, text = stringRes(R.string.follow)) {
|
||||||
text = { Text(stringRes(R.string.follow)) },
|
val author = note.author ?: return@M3ActionRow
|
||||||
onClick = {
|
|
||||||
val author = note.author ?: return@DropdownMenuItem
|
|
||||||
accountViewModel.follow(author)
|
accountViewModel.follow(author)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
|
||||||
} else {
|
} else {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.PersonRemove, text = stringRes(R.string.unfollow)) {
|
||||||
text = { Text(stringRes(R.string.unfollow)) },
|
val author = note.author ?: return@M3ActionRow
|
||||||
onClick = {
|
|
||||||
val author = note.author ?: return@DropdownMenuItem
|
|
||||||
accountViewModel.unfollow(author)
|
accountViewModel.unfollow(author)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
|
||||||
)
|
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
|
||||||
}
|
}
|
||||||
DropdownMenuItem(
|
}
|
||||||
text = { Text(text = stringRes(R.string.follow_set_add_author_from_note_action)) },
|
M3ActionRow(icon = Icons.Outlined.PlaylistAdd, text = stringRes(R.string.follow_set_add_author_from_note_action)) {
|
||||||
onClick = {
|
val authorHexKey = note.author?.pubkeyHex ?: return@M3ActionRow
|
||||||
val authorHexKey = note.author?.pubkeyHex ?: return@DropdownMenuItem
|
|
||||||
nav.nav(Route.PeopleListManagement(authorHexKey))
|
nav.nav(Route.PeopleListManagement(authorHexKey))
|
||||||
onDismiss()
|
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()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_user_pubkey)) {
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringRes(R.string.copy_user_pubkey)) },
|
|
||||||
onClick = {
|
|
||||||
note.author?.let {
|
note.author?.let {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
clipboardManager.setText(AnnotatedString("nostr:${it.pubkeyNpub()}"))
|
clipboardManager.setText(AnnotatedString("nostr:${it.pubkeyNpub()}"))
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_note_id)) {
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringRes(R.string.copy_note_id)) },
|
|
||||||
onClick = {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
clipboardManager.setText(AnnotatedString(note.toNostrUri()))
|
clipboardManager.setText(AnnotatedString(note.toNostrUri()))
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(icon = Icons.Outlined.Share, text = stringRes(R.string.quick_action_share)) {
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringRes(R.string.quick_action_share)) },
|
|
||||||
onClick = {
|
|
||||||
val sendIntent =
|
val sendIntent =
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
action = Intent.ACTION_SEND
|
action = Intent.ACTION_SEND
|
||||||
type = "text/plain"
|
type = "text/plain"
|
||||||
putExtra(
|
putExtra(Intent.EXTRA_TEXT, externalLinkForNote(note))
|
||||||
Intent.EXTRA_TEXT,
|
putExtra(Intent.EXTRA_TITLE, stringRes(actContext, R.string.quick_action_share_browser_link))
|
||||||
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)
|
ContextCompat.startActivity(actContext, shareIntent, null)
|
||||||
onDismiss()
|
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 (note.event is TextNoteEvent && !note.isDraft()) {
|
||||||
if (state.isLoggedUser) {
|
if (state.isLoggedUser) {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.edit_post)) {
|
||||||
text = { Text(stringRes(R.string.edit_post)) },
|
|
||||||
onClick = {
|
|
||||||
wantsToEditPost.value = true
|
wantsToEditPost.value = true
|
||||||
},
|
}
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.propose_an_edit)) {
|
||||||
text = { Text(stringRes(R.string.propose_an_edit)) },
|
|
||||||
onClick = {
|
|
||||||
wantsToEditPost.value = true
|
wantsToEditPost.value = true
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DropdownMenuItem(
|
}
|
||||||
text = { Text(stringRes(R.string.broadcast)) },
|
M3ActionRow(icon = Icons.Outlined.CellTower, text = stringRes(R.string.broadcast)) {
|
||||||
onClick = {
|
|
||||||
accountViewModel.broadcast(note)
|
accountViewModel.broadcast(note)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
|
||||||
|
// Timestamp & Bookmarks section
|
||||||
|
M3ActionSection {
|
||||||
if (accountViewModel.account.otsState.hasPendingAttestations(note)) {
|
if (accountViewModel.account.otsState.hasPendingAttestations(note)) {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Schedule, text = stringRes(R.string.timestamp_pending)) { onDismiss() }
|
||||||
text = { Text(stringRes(R.string.timestamp_pending)) },
|
|
||||||
onClick = {
|
|
||||||
onDismiss()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Schedule, text = stringRes(R.string.timestamp_it)) {
|
||||||
text = { Text(stringRes(R.string.timestamp_it)) },
|
|
||||||
onClick = {
|
|
||||||
accountViewModel.timestamp(note)
|
accountViewModel.timestamp(note)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
}
|
||||||
note.let {
|
|
||||||
val noteBookmarkType = if (note.event is LongTextNoteEvent) stringRes(R.string.article) else stringRes(R.string.post)
|
val noteBookmarkType = if (note.event is LongTextNoteEvent) stringRes(R.string.article) else stringRes(R.string.post)
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.BookmarkAdd, text = stringRes(R.string.manage_bookmark_label, noteBookmarkType)) {
|
||||||
text = { Text(stringRes(R.string.manage_bookmark_label, noteBookmarkType)) },
|
|
||||||
onClick = {
|
|
||||||
if (note.event is LongTextNoteEvent) {
|
if (note.event is LongTextNoteEvent) {
|
||||||
val noteAddress = (note as AddressableNote).address
|
nav.nav(Route.ArticleBookmarkManagement((note as AddressableNote).address))
|
||||||
nav.nav(Route.ArticleBookmarkManagement(noteAddress))
|
|
||||||
} else {
|
} else {
|
||||||
nav.nav(Route.PostBookmarkManagement(note.idHex))
|
nav.nav(Route.PostBookmarkManagement(note.idHex))
|
||||||
}
|
}
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (state.isPrivateBookmarkNote) {
|
if (state.isPrivateBookmarkNote) {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.LockOpen, text = stringRes(R.string.remove_from_private_bookmarks)) {
|
||||||
text = { Text(stringRes(R.string.remove_from_private_bookmarks)) },
|
|
||||||
onClick = {
|
|
||||||
accountViewModel.removePrivateBookmark(note)
|
accountViewModel.removePrivateBookmark(note)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Lock, text = stringRes(R.string.add_to_private_bookmarks)) {
|
||||||
text = { Text(stringRes(R.string.add_to_private_bookmarks)) },
|
|
||||||
onClick = {
|
|
||||||
accountViewModel.addPrivateBookmark(note)
|
accountViewModel.addPrivateBookmark(note)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (state.isPublicBookmarkNote) {
|
if (state.isPublicBookmarkNote) {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.BookmarkRemove, text = stringRes(R.string.remove_from_public_bookmarks)) {
|
||||||
text = { Text(stringRes(R.string.remove_from_public_bookmarks)) },
|
|
||||||
onClick = {
|
|
||||||
accountViewModel.removePublicBookmark(note)
|
accountViewModel.removePublicBookmark(note)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Bookmark, text = stringRes(R.string.add_to_public_bookmarks)) {
|
||||||
text = { Text(stringRes(R.string.add_to_public_bookmarks)) },
|
|
||||||
onClick = {
|
|
||||||
accountViewModel.addPublicBookmark(note)
|
accountViewModel.addPublicBookmark(note)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moderation section
|
||||||
|
M3ActionSection {
|
||||||
if (state.isLoggedUser) {
|
if (state.isLoggedUser) {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Delete, text = stringRes(R.string.request_deletion), isDestructive = true) {
|
||||||
text = { Text(stringRes(R.string.request_deletion)) },
|
|
||||||
onClick = {
|
|
||||||
accountViewModel.delete(note)
|
accountViewModel.delete(note)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Report, text = stringRes(R.string.block_report), isDestructive = true) {
|
||||||
text = { Text(stringRes(R.string.block_report)) },
|
reportDialogShowing = true
|
||||||
onClick = { 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
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.display
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material.icons.outlined.BookmarkRemove
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material.icons.outlined.CellTower
|
||||||
import androidx.compose.material3.Text
|
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.Composable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.getValue
|
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.actions.EditPostView
|
||||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
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.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo
|
import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo
|
||||||
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
|
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.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.report.ReportNoteDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.report.ReportNoteDialog
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
|
||||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -76,6 +87,7 @@ fun BookmarkGroupItemOptions(
|
|||||||
onClick = { popupExpanded.value = true },
|
onClick = { popupExpanded.value = true },
|
||||||
) {
|
) {
|
||||||
VerticalDotsIcon()
|
VerticalDotsIcon()
|
||||||
|
}
|
||||||
|
|
||||||
if (popupExpanded.value) {
|
if (popupExpanded.value) {
|
||||||
BookmarkGroupItemOptionsMenu(
|
BookmarkGroupItemOptionsMenu(
|
||||||
@@ -91,7 +103,6 @@ fun BookmarkGroupItemOptions(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BookmarkGroupItemOptionsMenu(
|
fun BookmarkGroupItemOptionsMenu(
|
||||||
@@ -142,89 +153,71 @@ fun BookmarkGroupItemOptionsMenu(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
M3ActionDialog(
|
||||||
expanded = true,
|
title = stringRes(R.string.bookmark_item_actions_dialog_title),
|
||||||
onDismissRequest = onDismiss,
|
onDismiss = onDismiss,
|
||||||
) {
|
) {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
val actContext = LocalContext.current
|
val actContext = LocalContext.current
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringRes(if (isBookmarkItemPrivate) R.string.move_bookmark_to_public_label else R.string.move_bookmark_to_private_label)) },
|
// Bookmark Management section
|
||||||
onClick = if (isBookmarkItemPrivate) onMoveBookmarkToPublic else onMoveBookmarkToPrivate,
|
M3ActionSection {
|
||||||
)
|
M3ActionRow(
|
||||||
DropdownMenuItem(
|
icon = if (isBookmarkItemPrivate) Icons.Outlined.LockOpen else Icons.Outlined.Lock,
|
||||||
text = { Text(stringRes(R.string.bookmark_remove_action_label)) },
|
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()
|
||||||
|
}
|
||||||
|
M3ActionRow(
|
||||||
|
icon = Icons.Outlined.BookmarkRemove,
|
||||||
|
text = stringRes(R.string.bookmark_remove_action_label),
|
||||||
|
isDestructive = true,
|
||||||
|
) {
|
||||||
onDeleteBookmarkItem()
|
onDeleteBookmarkItem()
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
|
||||||
|
|
||||||
|
// Follow section
|
||||||
|
M3ActionSection {
|
||||||
if (!state.isFollowingAuthor) {
|
if (!state.isFollowingAuthor) {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.PersonAdd, text = stringRes(R.string.follow)) {
|
||||||
text = { Text(stringRes(R.string.follow)) },
|
val author = note.author ?: return@M3ActionRow
|
||||||
onClick = {
|
|
||||||
val author = note.author ?: return@DropdownMenuItem
|
|
||||||
accountViewModel.follow(author)
|
accountViewModel.follow(author)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
|
||||||
} else {
|
} else {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.PersonRemove, text = stringRes(R.string.unfollow)) {
|
||||||
text = { Text(stringRes(R.string.unfollow)) },
|
val author = note.author ?: return@M3ActionRow
|
||||||
onClick = {
|
|
||||||
val author = note.author ?: return@DropdownMenuItem
|
|
||||||
accountViewModel.unfollow(author)
|
accountViewModel.unfollow(author)
|
||||||
onDismiss()
|
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
|
// Copy & Share section
|
||||||
// nav.nav(Route.PeopleListManagement(authorHexKey))
|
M3ActionSection {
|
||||||
// onDismiss()
|
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_text)) {
|
||||||
// },
|
|
||||||
// )
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringRes(R.string.copy_text)) },
|
|
||||||
onClick = {
|
|
||||||
val lastNoteVersion = (editState?.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value ?: note
|
val lastNoteVersion = (editState?.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value ?: note
|
||||||
accountViewModel.decrypt(lastNoteVersion) {
|
accountViewModel.decrypt(lastNoteVersion) { clipboardManager.setText(AnnotatedString(it)) }
|
||||||
clipboardManager.setText(AnnotatedString(it))
|
|
||||||
}
|
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_user_pubkey)) {
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringRes(R.string.copy_user_pubkey)) },
|
|
||||||
onClick = {
|
|
||||||
note.author?.let {
|
note.author?.let {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
clipboardManager.setText(AnnotatedString("nostr:${it.pubkeyNpub()}"))
|
clipboardManager.setText(AnnotatedString("nostr:${it.pubkeyNpub()}"))
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_note_id)) {
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringRes(R.string.copy_note_id)) },
|
|
||||||
onClick = {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
clipboardManager.setText(AnnotatedString(note.toNostrUri()))
|
clipboardManager.setText(AnnotatedString(note.toNostrUri()))
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(icon = Icons.Outlined.Share, text = stringRes(R.string.quick_action_share)) {
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringRes(R.string.quick_action_share)) },
|
|
||||||
onClick = {
|
|
||||||
val sendIntent =
|
val sendIntent =
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
action = Intent.ACTION_SEND
|
action = Intent.ACTION_SEND
|
||||||
@@ -243,74 +236,57 @@ fun BookmarkGroupItemOptionsMenu(
|
|||||||
Intent.createChooser(sendIntent, stringRes(actContext, R.string.quick_action_share))
|
Intent.createChooser(sendIntent, stringRes(actContext, R.string.quick_action_share))
|
||||||
ContextCompat.startActivity(actContext, shareIntent, null)
|
ContextCompat.startActivity(actContext, shareIntent, null)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
|
||||||
|
// Edit & Broadcast section
|
||||||
|
M3ActionSection {
|
||||||
if (state.isLoggedUser && note.isDraft()) {
|
if (state.isLoggedUser && note.isDraft()) {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.edit_draft)) {
|
||||||
text = { Text(stringRes(R.string.edit_draft)) },
|
|
||||||
onClick = {
|
|
||||||
nav.nav {
|
nav.nav {
|
||||||
routeEditDraftTo(note, accountViewModel.account)
|
routeEditDraftTo(note, accountViewModel.account)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (note.event is TextNoteEvent && !note.isDraft()) {
|
if (note.event is TextNoteEvent && !note.isDraft()) {
|
||||||
if (state.isLoggedUser) {
|
if (state.isLoggedUser) {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.edit_post)) {
|
||||||
text = { Text(stringRes(R.string.edit_post)) },
|
|
||||||
onClick = {
|
|
||||||
wantsToEditPost.value = true
|
wantsToEditPost.value = true
|
||||||
},
|
}
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.propose_an_edit)) {
|
||||||
text = { Text(stringRes(R.string.propose_an_edit)) },
|
|
||||||
onClick = {
|
|
||||||
wantsToEditPost.value = true
|
wantsToEditPost.value = true
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DropdownMenuItem(
|
}
|
||||||
text = { Text(stringRes(R.string.broadcast)) },
|
M3ActionRow(icon = Icons.Outlined.CellTower, text = stringRes(R.string.broadcast)) {
|
||||||
onClick = {
|
|
||||||
accountViewModel.broadcast(note)
|
accountViewModel.broadcast(note)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
|
||||||
|
// Timestamp & Moderation section
|
||||||
|
M3ActionSection {
|
||||||
if (accountViewModel.account.otsState.hasPendingAttestations(note)) {
|
if (accountViewModel.account.otsState.hasPendingAttestations(note)) {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Schedule, text = stringRes(R.string.timestamp_pending)) {
|
||||||
text = { Text(stringRes(R.string.timestamp_pending)) },
|
|
||||||
onClick = {
|
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Schedule, text = stringRes(R.string.timestamp_it)) {
|
||||||
text = { Text(stringRes(R.string.timestamp_it)) },
|
|
||||||
onClick = {
|
|
||||||
accountViewModel.timestamp(note)
|
accountViewModel.timestamp(note)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
}
|
||||||
if (state.isLoggedUser) {
|
if (state.isLoggedUser) {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Delete, text = stringRes(R.string.request_deletion), isDestructive = true) {
|
||||||
text = { Text(stringRes(R.string.request_deletion)) },
|
|
||||||
onClick = {
|
|
||||||
accountViewModel.delete(note)
|
accountViewModel.delete(note)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Report, text = stringRes(R.string.block_report), isDestructive = true) {
|
||||||
text = { Text(stringRes(R.string.block_report)) },
|
reportDialogShowing = true
|
||||||
onClick = { reportDialogShowing = true },
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+31
-43
@@ -30,11 +30,11 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.pager.PagerState
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
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.ButtonDefaults
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -57,6 +57,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
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.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar
|
||||||
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
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.screen.loggedIn.bookmarkgroups.BookmarkType
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
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.StdPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -304,6 +306,31 @@ fun BookmarkGroupActionsMenuButton(
|
|||||||
) {
|
) {
|
||||||
val isActionListOpen = remember { mutableStateOf(false) }
|
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(
|
ClickableBox(
|
||||||
modifier =
|
modifier =
|
||||||
StdPadding
|
StdPadding
|
||||||
@@ -319,44 +346,5 @@ fun BookmarkGroupActionsMenuButton(
|
|||||||
onClick = { isActionListOpen.value = true },
|
onClick = { isActionListOpen.value = true },
|
||||||
) {
|
) {
|
||||||
VerticalDotsIcon()
|
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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.Article
|
import androidx.compose.material.icons.automirrored.outlined.Article
|
||||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
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.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
@@ -57,6 +59,9 @@ import androidx.compose.ui.unit.sp
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
|
import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
|
||||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
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.note.VerticalDotsIcon
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -258,6 +263,7 @@ private fun BookmarkGroupOptionsButton(
|
|||||||
onClick = { isMenuOpen.value = true },
|
onClick = { isMenuOpen.value = true },
|
||||||
) {
|
) {
|
||||||
VerticalDotsIcon()
|
VerticalDotsIcon()
|
||||||
|
}
|
||||||
|
|
||||||
GroupOptionsMenu(
|
GroupOptionsMenu(
|
||||||
groupName = bookmarkGroupName,
|
groupName = bookmarkGroupName,
|
||||||
@@ -270,7 +276,6 @@ private fun BookmarkGroupOptionsButton(
|
|||||||
onDelete = onGroupDelete,
|
onDelete = onGroupDelete,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun GroupOptionsMenu(
|
private fun GroupOptionsMenu(
|
||||||
@@ -288,45 +293,32 @@ private fun GroupOptionsMenu(
|
|||||||
val optionalCloneName = remember { mutableStateOf<String?>(null) }
|
val optionalCloneName = remember { mutableStateOf<String?>(null) }
|
||||||
val optionalCloneDescription = remember { mutableStateOf<String?>(null) }
|
val optionalCloneDescription = remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
DropdownMenu(
|
if (isExpanded) {
|
||||||
expanded = isExpanded,
|
M3ActionDialog(
|
||||||
onDismissRequest = onDismiss,
|
title = stringRes(R.string.group_actions_dialog_title),
|
||||||
|
onDismiss = onDismiss,
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
M3ActionSection {
|
||||||
text = {
|
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.follow_set_rename_btn_label)) {
|
||||||
Text(text = stringRes(R.string.follow_set_rename_btn_label))
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
onGroupRename()
|
onGroupRename()
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(icon = Icons.Outlined.Description, text = stringRes(R.string.follow_set_desc_modify_label)) {
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text(text = stringRes(R.string.follow_set_desc_modify_label))
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
onGroupDescriptionChange()
|
onGroupDescriptionChange()
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.follow_set_copy_action_btn_label)) {
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text(text = stringRes(R.string.follow_set_copy_action_btn_label))
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
isCopyDialogOpen.value = true
|
isCopyDialogOpen.value = true
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
DropdownMenuItem(
|
M3ActionSection {
|
||||||
text = {
|
M3ActionRow(icon = Icons.Outlined.Delete, text = stringRes(R.string.quick_action_delete), isDestructive = true) {
|
||||||
Text(text = stringRes(R.string.quick_action_delete))
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
onDelete()
|
onDelete()
|
||||||
},
|
onDismiss()
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCopyDialogOpen.value) {
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.BookmarkAdd
|
import androidx.compose.material.icons.filled.BookmarkAdd
|
||||||
import androidx.compose.material.icons.filled.BookmarkRemove
|
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.CollectionsBookmark
|
||||||
import androidx.compose.material.icons.outlined.Lock
|
import androidx.compose.material.icons.outlined.Lock
|
||||||
import androidx.compose.material.icons.outlined.Public
|
import androidx.compose.material.icons.outlined.Public
|
||||||
import androidx.compose.material.icons.outlined.RemoveCircleOutline
|
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.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
@@ -48,6 +47,9 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import com.vitorpamplona.amethyst.R
|
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.screen.loggedIn.bookmarkgroups.list.BookmarkMembershipStatusAndNumberDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HalfHalfVertPadding
|
import com.vitorpamplona.amethyst.ui.theme.HalfHalfVertPadding
|
||||||
@@ -158,6 +160,30 @@ fun BookmarkManagementOptions(
|
|||||||
) {
|
) {
|
||||||
val isBookmarkAddTapped = remember { mutableStateOf(false) }
|
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(
|
Column(
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
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.foundation.pager.PagerState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material.icons.outlined.DoneAll
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material.icons.outlined.Groups
|
||||||
|
import androidx.compose.material.icons.outlined.MoveToInbox
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -48,6 +49,9 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
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.components.zonedDrawerSwipe
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
@@ -103,12 +107,13 @@ fun MessagesTabHeader(
|
|||||||
contentDescription = stringRes(id = R.string.more_options),
|
contentDescription = stringRes(id = R.string.more_options),
|
||||||
tint = MaterialTheme.colorScheme.placeholderText,
|
tint = MaterialTheme.colorScheme.placeholderText,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
MessagesTabMenu(
|
if (moreActionsExpanded) {
|
||||||
moreActionsExpanded,
|
MessagesMarkAsReadDialog(
|
||||||
{ moreActionsExpanded = false },
|
onDismiss = { moreActionsExpanded = false },
|
||||||
onMarkKnownAsRead,
|
onMarkKnownAsRead = onMarkKnownAsRead,
|
||||||
onMarkNewAsRead,
|
onMarkNewAsRead = onMarkNewAsRead,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,34 +147,38 @@ fun MessagesPager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessagesTabMenu(
|
fun MessagesMarkAsReadDialog(
|
||||||
expanded: Boolean,
|
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onMarkKnownAsRead: () -> Unit,
|
onMarkKnownAsRead: () -> Unit,
|
||||||
onMarkNewAsRead: () -> Unit,
|
onMarkNewAsRead: () -> Unit,
|
||||||
) {
|
) {
|
||||||
DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) {
|
M3ActionDialog(
|
||||||
DropdownMenuItem(
|
title = stringRes(R.string.mark_as_read_dialog_title),
|
||||||
text = { Text(stringRes(R.string.mark_all_known_as_read)) },
|
onDismiss = onDismiss,
|
||||||
onClick = {
|
) {
|
||||||
|
M3ActionSection {
|
||||||
|
M3ActionRow(
|
||||||
|
icon = Icons.Outlined.Groups,
|
||||||
|
text = stringRes(R.string.mark_all_known_as_read),
|
||||||
|
) {
|
||||||
onMarkKnownAsRead()
|
onMarkKnownAsRead()
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(
|
||||||
DropdownMenuItem(
|
icon = Icons.Outlined.MoveToInbox,
|
||||||
text = { Text(stringRes(R.string.mark_all_new_as_read)) },
|
text = stringRes(R.string.mark_all_new_as_read),
|
||||||
onClick = {
|
) {
|
||||||
onMarkNewAsRead()
|
onMarkNewAsRead()
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(
|
||||||
DropdownMenuItem(
|
icon = Icons.Outlined.DoneAll,
|
||||||
text = { Text(stringRes(R.string.mark_all_as_read)) },
|
text = stringRes(R.string.mark_all_as_read),
|
||||||
onClick = {
|
) {
|
||||||
onMarkKnownAsRead()
|
onMarkKnownAsRead()
|
||||||
onMarkNewAsRead()
|
onMarkNewAsRead()
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource
|
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.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
@@ -42,6 +43,7 @@ val CommunityPostKinds =
|
|||||||
ClassifiedsEvent.KIND,
|
ClassifiedsEvent.KIND,
|
||||||
LongTextNoteEvent.KIND,
|
LongTextNoteEvent.KIND,
|
||||||
CommunityPostApprovalEvent.KIND,
|
CommunityPostApprovalEvent.KIND,
|
||||||
|
AttestationEvent.KIND,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun filterCommunityPosts(
|
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.AdditiveFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||||
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
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.header.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||||
@@ -60,6 +64,7 @@ class HomeNewThreadFeedFilter(
|
|||||||
LongTextNoteEvent.KIND,
|
LongTextNoteEvent.KIND,
|
||||||
LiveChessGameChallengeEvent.KIND,
|
LiveChessGameChallengeEvent.KIND,
|
||||||
LiveChessGameEndEvent.KIND,
|
LiveChessGameEndEvent.KIND,
|
||||||
|
AttestationEvent.KIND,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +131,11 @@ class HomeNewThreadFeedFilter(
|
|||||||
noteEvent is AudioHeaderEvent ||
|
noteEvent is AudioHeaderEvent ||
|
||||||
noteEvent is ChessGameEvent ||
|
noteEvent is ChessGameEvent ||
|
||||||
noteEvent is LiveChessGameChallengeEvent ||
|
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) &&
|
filterParams.match(noteEvent, it.relays) &&
|
||||||
it.isNewThread()
|
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.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet
|
||||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.filterHomePostsByScopes
|
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.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
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.nip84Highlights.HighlightEvent
|
||||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
|
||||||
|
|
||||||
val HomePostsByGeohashKinds =
|
val HomePostsByGeohashKinds =
|
||||||
listOf(
|
listOf(
|
||||||
@@ -46,11 +46,11 @@ val HomePostsByGeohashKinds =
|
|||||||
LongTextNoteEvent.KIND,
|
LongTextNoteEvent.KIND,
|
||||||
ClassifiedsEvent.KIND,
|
ClassifiedsEvent.KIND,
|
||||||
HighlightEvent.KIND,
|
HighlightEvent.KIND,
|
||||||
InteractiveStoryPrologueEvent.KIND,
|
|
||||||
WikiNoteEvent.KIND,
|
WikiNoteEvent.KIND,
|
||||||
CommentEvent.KIND,
|
CommentEvent.KIND,
|
||||||
VoiceEvent.KIND,
|
VoiceEvent.KIND,
|
||||||
VoiceReplyEvent.KIND,
|
AttestationEvent.KIND,
|
||||||
|
InteractiveStoryPrologueEvent.KIND,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun filterHomePostsByGeohashes(
|
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.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet
|
||||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
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.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.client.pool.RelayBasedFilter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
|
|
||||||
@@ -43,11 +44,20 @@ fun filterHomePostsByGlobal(
|
|||||||
relay = relayUrl,
|
relay = relayUrl,
|
||||||
filter =
|
filter =
|
||||||
Filter(
|
Filter(
|
||||||
kinds = HomePostsNewThreadKinds,
|
kinds = HomePostsNewThreadKinds1,
|
||||||
limit = 50,
|
limit = 50,
|
||||||
since = since ?: newThreadSince,
|
since = since ?: newThreadSince,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
RelayBasedFilter(
|
||||||
|
relay = relayUrl,
|
||||||
|
filter =
|
||||||
|
Filter(
|
||||||
|
kinds = HomePostsNewThreadKinds2,
|
||||||
|
limit = 5,
|
||||||
|
since = since ?: newThreadSince,
|
||||||
|
),
|
||||||
|
),
|
||||||
RelayBasedFilter(
|
RelayBasedFilter(
|
||||||
relay = relayUrl,
|
relay = relayUrl,
|
||||||
filter =
|
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.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet
|
||||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.filterHomePostsByScopes
|
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.audio.header.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||||
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
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.nip84Highlights.HighlightEvent
|
||||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
|
||||||
|
|
||||||
val HomePostsBuHashtagsKinds =
|
val HomePostsBuHashtagsKinds =
|
||||||
listOf(
|
listOf(
|
||||||
@@ -50,12 +50,12 @@ val HomePostsBuHashtagsKinds =
|
|||||||
ClassifiedsEvent.KIND,
|
ClassifiedsEvent.KIND,
|
||||||
HighlightEvent.KIND,
|
HighlightEvent.KIND,
|
||||||
AudioHeaderEvent.KIND,
|
AudioHeaderEvent.KIND,
|
||||||
InteractiveStoryPrologueEvent.KIND,
|
|
||||||
CommentEvent.KIND,
|
CommentEvent.KIND,
|
||||||
WikiNoteEvent.KIND,
|
WikiNoteEvent.KIND,
|
||||||
NipTextEvent.KIND,
|
NipTextEvent.KIND,
|
||||||
VoiceEvent.KIND,
|
VoiceEvent.KIND,
|
||||||
VoiceReplyEvent.KIND,
|
AttestationEvent.KIND,
|
||||||
|
InteractiveStoryPrologueEvent.KIND,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun filterHomePostsByHashtags(
|
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.author.AuthorsTopNavPerRelayFilterSet
|
||||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet
|
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet
|
||||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
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.ephemChat.chat.EphemeralChatEvent
|
||||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||||
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
||||||
@@ -50,7 +51,7 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
|||||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
val HomePostsNewThreadKinds =
|
val HomePostsNewThreadKinds1 =
|
||||||
listOf(
|
listOf(
|
||||||
TextNoteEvent.KIND,
|
TextNoteEvent.KIND,
|
||||||
RepostEvent.KIND,
|
RepostEvent.KIND,
|
||||||
@@ -58,10 +59,15 @@ val HomePostsNewThreadKinds =
|
|||||||
ClassifiedsEvent.KIND,
|
ClassifiedsEvent.KIND,
|
||||||
LongTextNoteEvent.KIND,
|
LongTextNoteEvent.KIND,
|
||||||
HighlightEvent.KIND,
|
HighlightEvent.KIND,
|
||||||
WikiNoteEvent.KIND,
|
|
||||||
NipTextEvent.KIND,
|
|
||||||
ZapPollEvent.KIND,
|
ZapPollEvent.KIND,
|
||||||
PollEvent.KIND,
|
PollEvent.KIND,
|
||||||
|
WikiNoteEvent.KIND,
|
||||||
|
AttestationEvent.KIND,
|
||||||
|
NipTextEvent.KIND,
|
||||||
|
)
|
||||||
|
|
||||||
|
val HomePostsNewThreadKinds2 =
|
||||||
|
listOf(
|
||||||
InteractiveStoryPrologueEvent.KIND,
|
InteractiveStoryPrologueEvent.KIND,
|
||||||
ChessGameEvent.KIND,
|
ChessGameEvent.KIND,
|
||||||
LiveChessGameChallengeEvent.KIND,
|
LiveChessGameChallengeEvent.KIND,
|
||||||
@@ -91,12 +97,22 @@ fun filterNewHomePostsByAuthors(
|
|||||||
relay = relay,
|
relay = relay,
|
||||||
filter =
|
filter =
|
||||||
Filter(
|
Filter(
|
||||||
kinds = HomePostsNewThreadKinds,
|
kinds = HomePostsNewThreadKinds1,
|
||||||
authors = authorList,
|
authors = authorList,
|
||||||
limit = min(authorList.size * 10, 500),
|
limit = min(authorList.size * 10, 500),
|
||||||
since = since,
|
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.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.PagerState
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
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.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults.cardElevation
|
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.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.IconButton
|
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.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
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.EmptyNav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
@@ -416,15 +422,16 @@ private fun ListActionsMenuButton(
|
|||||||
onClick = { isActionListOpen.value = true },
|
onClick = { isActionListOpen.value = true },
|
||||||
) {
|
) {
|
||||||
VerticalDotsIcon()
|
VerticalDotsIcon()
|
||||||
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
if (isActionListOpen.value) {
|
||||||
expanded = isActionListOpen.value,
|
|
||||||
onDismissRequest = { isActionListOpen.value = false },
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
DropdownMenuItem(
|
M3ActionDialog(
|
||||||
text = { Text(stringRes(R.string.quick_action_share)) },
|
title = stringRes(R.string.list_actions_dialog_title),
|
||||||
onClick = {
|
onDismiss = { isActionListOpen.value = false },
|
||||||
|
) {
|
||||||
|
M3ActionSection {
|
||||||
|
M3ActionRow(icon = Icons.Outlined.Share, text = stringRes(R.string.quick_action_share)) {
|
||||||
val sendIntent =
|
val sendIntent =
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
action = Intent.ACTION_SEND
|
action = Intent.ACTION_SEND
|
||||||
@@ -443,38 +450,22 @@ private fun ListActionsMenuButton(
|
|||||||
Intent.createChooser(sendIntent, stringRes(context, R.string.quick_action_share))
|
Intent.createChooser(sendIntent, stringRes(context, R.string.quick_action_share))
|
||||||
ContextCompat.startActivity(context, shareIntent, null)
|
ContextCompat.startActivity(context, shareIntent, null)
|
||||||
isActionListOpen.value = false
|
isActionListOpen.value = false
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.follow_set_edit_list_metadata)) {
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text(stringRes(R.string.follow_set_edit_list_metadata))
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
onEditList()
|
onEditList()
|
||||||
isActionListOpen.value = false
|
isActionListOpen.value = false
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(icon = Icons.Outlined.CellTower, text = stringRes(R.string.follow_set_broadcast)) {
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text(stringRes(R.string.follow_set_broadcast))
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
onBroadcastList()
|
onBroadcastList()
|
||||||
isActionListOpen.value = false
|
isActionListOpen.value = false
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
M3ActionSection {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Delete, text = stringRes(R.string.follow_set_delete), isDestructive = true) {
|
||||||
text = {
|
|
||||||
Text(stringRes(R.string.follow_set_delete))
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
onDeleteList()
|
onDeleteList()
|
||||||
isActionListOpen.value = false
|
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.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
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.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults.cardElevation
|
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.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.IconButton
|
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.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
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.EmptyNav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
@@ -271,15 +277,16 @@ private fun ListActionsMenuButton(
|
|||||||
onClick = { isActionListOpen.value = true },
|
onClick = { isActionListOpen.value = true },
|
||||||
) {
|
) {
|
||||||
VerticalDotsIcon()
|
VerticalDotsIcon()
|
||||||
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
if (isActionListOpen.value) {
|
||||||
expanded = isActionListOpen.value,
|
|
||||||
onDismissRequest = { isActionListOpen.value = false },
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
DropdownMenuItem(
|
M3ActionDialog(
|
||||||
text = { Text(stringRes(R.string.quick_action_share)) },
|
title = stringRes(R.string.pack_actions_dialog_title),
|
||||||
onClick = {
|
onDismiss = { isActionListOpen.value = false },
|
||||||
|
) {
|
||||||
|
M3ActionSection {
|
||||||
|
M3ActionRow(icon = Icons.Outlined.Share, text = stringRes(R.string.quick_action_share)) {
|
||||||
val sendIntent =
|
val sendIntent =
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
action = Intent.ACTION_SEND
|
action = Intent.ACTION_SEND
|
||||||
@@ -298,38 +305,22 @@ private fun ListActionsMenuButton(
|
|||||||
Intent.createChooser(sendIntent, stringRes(context, R.string.quick_action_share))
|
Intent.createChooser(sendIntent, stringRes(context, R.string.quick_action_share))
|
||||||
ContextCompat.startActivity(context, shareIntent, null)
|
ContextCompat.startActivity(context, shareIntent, null)
|
||||||
isActionListOpen.value = false
|
isActionListOpen.value = false
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.follow_pack_edit_list_metadata)) {
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text(stringRes(R.string.follow_pack_edit_list_metadata))
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
onEditList()
|
onEditList()
|
||||||
isActionListOpen.value = false
|
isActionListOpen.value = false
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(icon = Icons.Outlined.CellTower, text = stringRes(R.string.follow_pack_broadcast)) {
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text(stringRes(R.string.follow_pack_broadcast))
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
onBroadcastList()
|
onBroadcastList()
|
||||||
isActionListOpen.value = false
|
isActionListOpen.value = false
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
M3ActionSection {
|
||||||
DropdownMenuItem(
|
M3ActionRow(icon = Icons.Outlined.Delete, text = stringRes(R.string.follow_pack_delete), isDestructive = true) {
|
||||||
text = {
|
|
||||||
Text(stringRes(R.string.follow_pack_delete))
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
onDeleteList()
|
onDeleteList()
|
||||||
isActionListOpen.value = false
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.material.icons.Icons
|
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.Groups
|
||||||
import androidx.compose.material.icons.outlined.Lock
|
import androidx.compose.material.icons.outlined.Lock
|
||||||
import androidx.compose.material.icons.outlined.Public
|
import androidx.compose.material.icons.outlined.Public
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.Text
|
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.User
|
||||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
||||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
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.note.VerticalDotsIcon
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||||
@@ -286,6 +290,7 @@ private fun PeopleListOptionsButton(
|
|||||||
onClick = { isMenuOpen.value = true },
|
onClick = { isMenuOpen.value = true },
|
||||||
) {
|
) {
|
||||||
VerticalDotsIcon()
|
VerticalDotsIcon()
|
||||||
|
}
|
||||||
|
|
||||||
ListOptionsMenu(
|
ListOptionsMenu(
|
||||||
isExpanded = isMenuOpen.value,
|
isExpanded = isMenuOpen.value,
|
||||||
@@ -295,7 +300,6 @@ private fun PeopleListOptionsButton(
|
|||||||
onDismiss = { isMenuOpen.value = false },
|
onDismiss = { isMenuOpen.value = false },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ListOptionsMenu(
|
private fun ListOptionsMenu(
|
||||||
@@ -309,36 +313,28 @@ private fun ListOptionsMenu(
|
|||||||
val optionalCloneName = remember { mutableStateOf<String?>(null) }
|
val optionalCloneName = remember { mutableStateOf<String?>(null) }
|
||||||
val optionalCloneDescription = remember { mutableStateOf<String?>(null) }
|
val optionalCloneDescription = remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
DropdownMenu(
|
if (isExpanded) {
|
||||||
expanded = isExpanded,
|
M3ActionDialog(
|
||||||
onDismissRequest = onDismiss,
|
title = stringRes(R.string.list_management_dialog_title),
|
||||||
|
onDismiss = onDismiss,
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
M3ActionSection {
|
||||||
text = {
|
M3ActionRow(icon = Icons.Outlined.Edit, text = stringRes(R.string.follow_set_edit_list_metadata)) {
|
||||||
Text(text = stringRes(R.string.follow_set_edit_list_metadata))
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
onListEditMetadata()
|
onListEditMetadata()
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.follow_set_copy_action_btn_label)) {
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text(text = stringRes(R.string.follow_set_copy_action_btn_label))
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
isCopyDialogOpen.value = true
|
isCopyDialogOpen.value = true
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
DropdownMenuItem(
|
M3ActionSection {
|
||||||
text = {
|
M3ActionRow(icon = Icons.Outlined.Delete, text = stringRes(R.string.quick_action_delete), isDestructive = true) {
|
||||||
Text(text = stringRes(R.string.quick_action_delete))
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
onDelete()
|
onDelete()
|
||||||
},
|
onDismiss()
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCopyDialogOpen.value) {
|
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.filled.PersonRemove
|
||||||
import androidx.compose.material.icons.outlined.Groups
|
import androidx.compose.material.icons.outlined.Groups
|
||||||
import androidx.compose.material.icons.outlined.Lock
|
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.Public
|
||||||
import androidx.compose.material.icons.outlined.RemoveCircleOutline
|
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.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.ListItem
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.vitorpamplona.amethyst.R
|
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.screen.loggedIn.lists.list.DisplayParticipantNumberAndStatus
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HalfHalfVertPadding
|
import com.vitorpamplona.amethyst.ui.theme.HalfHalfVertPadding
|
||||||
@@ -193,6 +195,30 @@ private fun UserAdditionOptions(
|
|||||||
) {
|
) {
|
||||||
val isUserAddTapped = remember { mutableStateOf(false) }
|
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(
|
Column(
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
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.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
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.StdVertSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RefreshableCardView(
|
fun RefreshableCardView(
|
||||||
@@ -165,6 +170,7 @@ private fun FeedLoaded(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val items by loaded.feed.collectAsStateWithLifecycle()
|
val items by loaded.feed.collectAsStateWithLifecycle()
|
||||||
|
val openPolls by openPollsState(accountViewModel)
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -175,6 +181,37 @@ private fun FeedLoaded(
|
|||||||
ShowDonationCard(accountViewModel, nav)
|
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(
|
itemsIndexed(
|
||||||
items = items.list,
|
items = items.list,
|
||||||
key = { _, item -> item.id() },
|
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.AdditiveFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||||
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
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.audio.track.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.experimental.forks.IForkableEvent
|
import com.vitorpamplona.quartz.experimental.forks.IForkableEvent
|
||||||
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
||||||
@@ -87,6 +88,7 @@ class NotificationFeedFilter(
|
|||||||
VideoVerticalEvent.KIND,
|
VideoVerticalEvent.KIND,
|
||||||
VideoHorizontalEvent.KIND,
|
VideoHorizontalEvent.KIND,
|
||||||
WikiNoteEvent.KIND,
|
WikiNoteEvent.KIND,
|
||||||
|
AttestationRequestEvent.KIND,
|
||||||
)
|
)
|
||||||
|
|
||||||
val NOTIFICATION_KINDS =
|
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.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
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.interactiveStories.InteractiveStoryPrologueEvent
|
||||||
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
||||||
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
|
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
|
||||||
@@ -66,6 +67,7 @@ val UserProfilePostKinds2 =
|
|||||||
VoiceReplyEvent.KIND,
|
VoiceReplyEvent.KIND,
|
||||||
ZapPollEvent.KIND,
|
ZapPollEvent.KIND,
|
||||||
PinListEvent.KIND,
|
PinListEvent.KIND,
|
||||||
|
AttestationEvent.KIND,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun filterUserProfilePosts(
|
fun filterUserProfilePosts(
|
||||||
|
|||||||
+61
-71
@@ -21,20 +21,24 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material.icons.outlined.Block
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material.icons.outlined.CheckCircle
|
||||||
import androidx.compose.material3.Text
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.User
|
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.note.externalLinkForUser
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
|
||||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -44,108 +48,94 @@ fun UserProfileDropDownMenu(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
DropdownMenu(
|
if (!popupExpanded) return
|
||||||
expanded = popupExpanded,
|
|
||||||
onDismissRequest = onDismiss,
|
M3ActionDialog(
|
||||||
|
title = stringRes(R.string.profile_actions_dialog_title),
|
||||||
|
onDismiss = onDismiss,
|
||||||
) {
|
) {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringRes(R.string.copy_user_id)) },
|
|
||||||
onClick = {
|
|
||||||
clipboardManager.setText(AnnotatedString(user.pubkeyNpub()))
|
|
||||||
onDismiss()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
DropdownMenuItem(
|
// Share section
|
||||||
text = { Text(stringRes(R.string.quick_action_share)) },
|
M3ActionSection {
|
||||||
onClick = {
|
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 =
|
val sendIntent =
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
action = Intent.ACTION_SEND
|
action = Intent.ACTION_SEND
|
||||||
type = "text/plain"
|
type = "text/plain"
|
||||||
putExtra(
|
putExtra(Intent.EXTRA_TEXT, externalLinkForUser(user))
|
||||||
Intent.EXTRA_TEXT,
|
|
||||||
externalLinkForUser(user),
|
|
||||||
)
|
|
||||||
putExtra(
|
putExtra(
|
||||||
Intent.EXTRA_TITLE,
|
Intent.EXTRA_TITLE,
|
||||||
stringRes(context, R.string.quick_action_share_browser_link),
|
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)
|
context.startActivity(shareIntent)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
|
// Moderation section (if not self)
|
||||||
if (accountViewModel.userProfile() != user) {
|
if (accountViewModel.userProfile() != user) {
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
M3ActionSection {
|
||||||
if (accountViewModel.account.isHidden(user)) {
|
if (accountViewModel.account.isHidden(user)) {
|
||||||
DropdownMenuItem(
|
M3ActionRow(
|
||||||
text = { Text(stringRes(R.string.unblock_user)) },
|
icon = Icons.Outlined.CheckCircle,
|
||||||
onClick = {
|
text = stringRes(R.string.unblock_user),
|
||||||
|
) {
|
||||||
accountViewModel.show(user)
|
accountViewModel.show(user)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
DropdownMenuItem(
|
M3ActionRow(
|
||||||
text = { Text(stringRes(id = R.string.block_hide_user)) },
|
icon = Icons.Outlined.Block,
|
||||||
onClick = {
|
text = stringRes(R.string.block_hide_user),
|
||||||
|
isDestructive = true,
|
||||||
|
) {
|
||||||
accountViewModel.hide(user)
|
accountViewModel.hide(user)
|
||||||
onDismiss()
|
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)
|
accountViewModel.report(user, ReportType.SPAM)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(icon = Icons.Outlined.Report, text = stringRes(R.string.report_hateful_speech), isDestructive = true) {
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringRes(R.string.report_hateful_speech)) },
|
|
||||||
onClick = {
|
|
||||||
accountViewModel.report(user, ReportType.PROFANITY)
|
accountViewModel.report(user, ReportType.PROFANITY)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(icon = Icons.Outlined.Report, text = stringRes(R.string.report_impersonation), isDestructive = true) {
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringRes(id = R.string.report_impersonation)) },
|
|
||||||
onClick = {
|
|
||||||
accountViewModel.report(user, ReportType.IMPERSONATION)
|
accountViewModel.report(user, ReportType.IMPERSONATION)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(icon = Icons.Outlined.Report, text = stringRes(R.string.report_nudity_porn), isDestructive = true) {
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringRes(R.string.report_nudity_porn)) },
|
|
||||||
onClick = {
|
|
||||||
accountViewModel.report(user, ReportType.NUDITY)
|
accountViewModel.report(user, ReportType.NUDITY)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(icon = Icons.Outlined.Report, text = stringRes(R.string.report_illegal_behaviour), isDestructive = true) {
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringRes(id = R.string.report_illegal_behaviour)) },
|
|
||||||
onClick = {
|
|
||||||
accountViewModel.report(user, ReportType.ILLEGAL)
|
accountViewModel.report(user, ReportType.ILLEGAL)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(icon = Icons.Outlined.Report, text = stringRes(R.string.report_malware), isDestructive = true) {
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringRes(id = R.string.report_malware)) },
|
|
||||||
onClick = {
|
|
||||||
accountViewModel.report(user, ReportType.MALWARE)
|
accountViewModel.report(user, ReportType.MALWARE)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -27,6 +27,10 @@ import com.vitorpamplona.amethyst.model.Note
|
|||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
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.header.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||||
@@ -87,7 +91,11 @@ class UserProfileNewThreadFeedFilter(
|
|||||||
it.event is AudioTrackEvent ||
|
it.event is AudioTrackEvent ||
|
||||||
it.event is AudioHeaderEvent ||
|
it.event is AudioHeaderEvent ||
|
||||||
it.event is VoiceEvent ||
|
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() &&
|
it.isNewThread() &&
|
||||||
account.isAcceptable(it)
|
account.isAcceptable(it)
|
||||||
|
|||||||
+23
-16
@@ -29,8 +29,8 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Share
|
import androidx.compose.material.icons.filled.Share
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material.icons.outlined.Description
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material.icons.outlined.FolderZip
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
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.DefaultDMRelayList
|
||||||
import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList
|
import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList
|
||||||
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
|
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.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
@@ -490,6 +493,7 @@ fun SettingsCategoryWithButton(
|
|||||||
@Composable
|
@Composable
|
||||||
fun ExportDropdownMenu(collection: () -> RelayListCollection) {
|
fun ExportDropdownMenu(collection: () -> RelayListCollection) {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
IconButton(onClick = { expanded = true }) {
|
IconButton(onClick = { expanded = true }) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -498,24 +502,27 @@ fun ExportDropdownMenu(collection: () -> RelayListCollection) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
if (expanded) {
|
||||||
expanded = expanded,
|
M3ActionDialog(
|
||||||
onDismissRequest = { expanded = false },
|
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
|
expanded = false
|
||||||
RelayExporter(context).export(collection())
|
RelayExporter(context).export(collection())
|
||||||
},
|
}
|
||||||
)
|
M3ActionRow(
|
||||||
DropdownMenuItem(
|
icon = Icons.Outlined.FolderZip,
|
||||||
text = { Text(stringRes(R.string.export_as_zip)) },
|
text = stringRes(R.string.export_as_zip),
|
||||||
onClick = {
|
) {
|
||||||
expanded = false
|
expanded = false
|
||||||
RelayZipExporter(context).export(collection())
|
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.ThemeComparisonRow
|
||||||
import com.vitorpamplona.amethyst.ui.theme.bitcoinColor
|
import com.vitorpamplona.amethyst.ui.theme.bitcoinColor
|
||||||
import com.vitorpamplona.amethyst.ui.theme.redColorOnSecondSurface
|
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.header.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
|
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
|
||||||
@@ -529,6 +533,10 @@ private fun kindDisplayName(kind: Int): Int =
|
|||||||
AppSpecificDataEvent.KIND -> R.string.kind_user_settings
|
AppSpecificDataEvent.KIND -> R.string.kind_user_settings
|
||||||
AudioHeaderEvent.KIND -> R.string.kind_audio_header
|
AudioHeaderEvent.KIND -> R.string.kind_audio_header
|
||||||
AudioTrackEvent.KIND -> R.string.kind_audio_track
|
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
|
BadgeAwardEvent.KIND -> R.string.kind_badge_awards
|
||||||
BadgeDefinitionEvent.KIND -> R.string.kind_badge_definitions
|
BadgeDefinitionEvent.KIND -> R.string.kind_badge_definitions
|
||||||
BadgeProfilesEvent.KIND -> R.string.kind_profile_badges
|
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)
|
val reports = setOf(ReportEvent.KIND, MuteListEvent.KIND, DeletionEvent.KIND, RequestToVanishEvent.KIND)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun KindChip(kind: Int) {
|
fun KindChip(kind: Int) {
|
||||||
val nameResId = kindDisplayName(kind)
|
val nameResId = kindDisplayName(kind)
|
||||||
val name = if (nameResId != -1) stringResource(nameResId) else "k$kind"
|
val name = if (nameResId != -1) stringResource(nameResId) else "k$kind"
|
||||||
val (bg, fg) =
|
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.FileStorageHeaderDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.PictureDisplay
|
import com.vitorpamplona.amethyst.ui.note.types.PictureDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition
|
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.RenderCalendarDateSlotEvent
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarTimeSlotEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarTimeSlotEvent
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderChannelMessage
|
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.lessImportantLink
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.amethyst.ui.theme.selectedNote
|
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.header.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward
|
import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward
|
||||||
@@ -620,6 +628,14 @@ private fun FullBleedNoteCompose(
|
|||||||
accountViewModel,
|
accountViewModel,
|
||||||
nav,
|
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) {
|
} else if (noteEvent is AdvertisedRelayListEvent) {
|
||||||
DisplayNIP65RelayList(baseNote, backgroundColor, accountViewModel, nav)
|
DisplayNIP65RelayList(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
} else if (noteEvent is LnZapEvent) {
|
} else if (noteEvent is LnZapEvent) {
|
||||||
|
|||||||
@@ -661,4 +661,5 @@
|
|||||||
<string name="delete_all">حذف الكل</string>
|
<string name="delete_all">حذف الكل</string>
|
||||||
<string name="delete_all_drafts_confirmation">متأكد من حذف جميع المسودات؟</string>
|
<string name="delete_all_drafts_confirmation">متأكد من حذف جميع المسودات؟</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -573,4 +573,5 @@
|
|||||||
<string name="donate_now">এখনই দান করুন</string>
|
<string name="donate_now">এখনই দান করুন</string>
|
||||||
<string name="accessibility_scan_qr_code">QR কোড স্ক্যান করুন</string>
|
<string name="accessibility_scan_qr_code">QR কোড স্ক্যান করুন</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1310,6 +1310,10 @@
|
|||||||
<string name="my_lists">Moje seznamy</string>
|
<string name="my_lists">Moje seznamy</string>
|
||||||
<string name="people_list_label">Uživatelé</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="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="temporary_account">Odhlásit se na zámek zařízení</string>
|
||||||
<string name="private_message">Soukromá zpráva</string>
|
<string name="private_message">Soukromá zpráva</string>
|
||||||
<string name="public_message">Veřejná 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_downloading">Stahování</string>
|
||||||
<string name="event_sync_status_error">Chyba</string>
|
<string name="event_sync_status_error">Chyba</string>
|
||||||
<string name="event_sync_status_completed">Dokončeno</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>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1315,6 +1315,10 @@ anz der Bedingungen ist erforderlich</string>
|
|||||||
<string name="my_lists">Meine Listen</string>
|
<string name="my_lists">Meine Listen</string>
|
||||||
<string name="people_list_label">Benutzer</string>
|
<string name="people_list_label">Benutzer</string>
|
||||||
<string name="select_list_to_filter">Liste zum Filtern des Feeds auswählen</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="temporary_account">Beim Sperren des Geräts abmelden</string>
|
||||||
<string name="private_message">Private Nachricht</string>
|
<string name="private_message">Private Nachricht</string>
|
||||||
<string name="public_message">Öffentliche 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_downloading">Herunterladen</string>
|
||||||
<string name="event_sync_status_error">Fehler</string>
|
<string name="event_sync_status_error">Fehler</string>
|
||||||
<string name="event_sync_status_completed">Abgeschlossen</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>
|
</resources>
|
||||||
|
|||||||
@@ -515,4 +515,5 @@
|
|||||||
Μετά την εγκατάσταση, επιλέξτε την εφαρμογή που θέλετε να χρησιμοποιήσετε από τις Ρυθμίσεις.
|
Μετά την εγκατάσταση, επιλέξτε την εφαρμογή που θέλετε να χρησιμοποιήσετε από τις Ρυθμίσεις.
|
||||||
</string>
|
</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -8,4 +8,5 @@
|
|||||||
<string name="show_anyway">Show Anyway</string>
|
<string name="show_anyway">Show Anyway</string>
|
||||||
<string name="post_not_found_short">👀</string>
|
<string name="post_not_found_short">👀</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -443,4 +443,5 @@
|
|||||||
<string name="error_dialog_talk_to_user">Mesaĝi la Uzanto</string>
|
<string name="error_dialog_talk_to_user">Mesaĝi la Uzanto</string>
|
||||||
<string name="error_dialog_button_ok">Okej</string>
|
<string name="error_dialog_button_ok">Okej</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1183,4 +1183,5 @@
|
|||||||
<string name="this_message_will_disappear_in_days">Este mensaje desaparecerá en %1$d días</string>
|
<string name="this_message_will_disappear_in_days">Este mensaje desaparecerá en %1$d días</string>
|
||||||
<string name="select_signer">Seleccionar firmante</string>
|
<string name="select_signer">Seleccionar firmante</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</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="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>
|
<string name="select_signer">Seleccionar firmante</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</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="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>
|
<string name="select_signer">Seleccionar firmante</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -948,4 +948,5 @@
|
|||||||
<string name="temporary_account">با قفل کردن دستگاه از حساب کاربری خارج شو</string>
|
<string name="temporary_account">با قفل کردن دستگاه از حساب کاربری خارج شو</string>
|
||||||
<string name="private_message">پیام خصوصی</string>
|
<string name="private_message">پیام خصوصی</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -570,4 +570,5 @@
|
|||||||
<string name="could_not_download_from_the_server">Mediaa ei voitu ladata palvelimelta</string>
|
<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>
|
<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 -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1365,4 +1365,5 @@
|
|||||||
<string name="continue_txt">Continuer</string>
|
<string name="continue_txt">Continuer</string>
|
||||||
<string name="skip_for_now">Ignorer pour le moment</string>
|
<string name="skip_for_now">Ignorer pour le moment</string>
|
||||||
<string name="select_all">Tout sélectionner</string>
|
<string name="select_all">Tout sélectionner</string>
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1558,4 +1558,5 @@
|
|||||||
<string name="uptime">%1$d%% समय निरन्तर उपलब्ध</string>
|
<string name="uptime">%1$d%% समय निरन्तर उपलब्ध</string>
|
||||||
<string name="namecoin_settings">नामरूप्य स्थापना विकल्प</string>
|
<string name="namecoin_settings">नामरूप्य स्थापना विकल्प</string>
|
||||||
<string name="ots_explorer_settings">द्व्यंकरूप्य समन्वेषक (ओटीएस॰)</string>
|
<string name="ots_explorer_settings">द्व्यंकरूप्य समन्वेषक (ओटीएस॰)</string>
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1630,4 +1630,5 @@
|
|||||||
<string name="event_sync_status_downloading">Letöltés</string>
|
<string name="event_sync_status_downloading">Letöltés</string>
|
||||||
<string name="event_sync_status_error">Hiba</string>
|
<string name="event_sync_status_error">Hiba</string>
|
||||||
<string name="event_sync_status_completed">Kész</string>
|
<string name="event_sync_status_completed">Kész</string>
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -512,4 +512,5 @@ Seharusnya %3$s</string>
|
|||||||
Setelah diinstall, Pilih aplikasi yang ingin digunakan di pengaturan.
|
Setelah diinstall, Pilih aplikasi yang ingin digunakan di pengaturan.
|
||||||
</string>
|
</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -433,4 +433,5 @@
|
|||||||
<string name="active_for_global">Globale</string>
|
<string name="active_for_global">Globale</string>
|
||||||
<string name="active_for_search">Cerca</string>
|
<string name="active_for_search">Cerca</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -390,4 +390,5 @@
|
|||||||
<string name="automatically_show_url_preview">自動的にURLプレビューを表示</string>
|
<string name="automatically_show_url_preview">自動的にURLプレビューを表示</string>
|
||||||
<string name="load_image">画像読み込み</string>
|
<string name="load_image">画像読み込み</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -172,4 +172,5 @@
|
|||||||
<string name="torrent_download">Lejupielādēt</string>
|
<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>
|
<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 -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1145,4 +1145,5 @@
|
|||||||
<string name="follow_set_delete">Verwijder lijst</string>
|
<string name="follow_set_delete">Verwijder lijst</string>
|
||||||
<string name="follow_pack_delete">Verwijder pakket</string>
|
<string name="follow_pack_delete">Verwijder pakket</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1589,4 +1589,5 @@
|
|||||||
<string name="event_sync_status_downloading">Pobieranie</string>
|
<string name="event_sync_status_downloading">Pobieranie</string>
|
||||||
<string name="event_sync_status_error">Błąd</string>
|
<string name="event_sync_status_error">Błąd</string>
|
||||||
<string name="event_sync_status_completed">Zakończone</string>
|
<string name="event_sync_status_completed">Zakończone</string>
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1310,6 +1310,10 @@
|
|||||||
<string name="my_lists">Minhas listas</string>
|
<string name="my_lists">Minhas listas</string>
|
||||||
<string name="people_list_label">Usuários</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="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="temporary_account">Terminar sessão no bloqueio do dispositivo</string>
|
||||||
<string name="private_message">Mensagem Privada</string>
|
<string name="private_message">Mensagem Privada</string>
|
||||||
<string name="public_message">Mensagem pública</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_downloading">Baixando</string>
|
||||||
<string name="event_sync_status_error">Erro</string>
|
<string name="event_sync_status_error">Erro</string>
|
||||||
<string name="event_sync_status_completed">Concluído</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>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -432,4 +432,5 @@
|
|||||||
<string name="classifieds_category_misc">Разное</string>
|
<string name="classifieds_category_misc">Разное</string>
|
||||||
<string name="classifieds_category_other">Другое</string>
|
<string name="classifieds_category_other">Другое</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</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_downloading">Prenašam</string>
|
||||||
<string name="event_sync_status_error">Napaka</string>
|
<string name="event_sync_status_error">Napaka</string>
|
||||||
<string name="event_sync_status_completed">Zaključeno</string>
|
<string name="event_sync_status_completed">Zaključeno</string>
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -34,4 +34,5 @@
|
|||||||
<string name="login_with_a_private_key_to_like_posts">Користите јавни кључ и јавни кључеви су само за читање. Пријавите се са приватним кључем да бисте лајковали постове</string>
|
<string name="login_with_a_private_key_to_like_posts">Користите јавни кључ и јавни кључеви су само за читање. Пријавите се са приватним кључем да бисте лајковали постове</string>
|
||||||
<string name="no_zap_amount_setup_long_press_to_change">Нема подешавања износа Зап. Дуго притисните да бисте променили</string>
|
<string name="no_zap_amount_setup_long_press_to_change">Нема подешавања износа Зап. Дуго притисните да бисте променили</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1309,6 +1309,10 @@
|
|||||||
<string name="my_lists">Mina listor</string>
|
<string name="my_lists">Mina listor</string>
|
||||||
<string name="people_list_label">Användare</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="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="temporary_account">Logga ut när enheten låses</string>
|
||||||
<string name="private_message">Privat meddelande</string>
|
<string name="private_message">Privat meddelande</string>
|
||||||
<string name="public_message">Offentligt 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_downloading">Laddar ner</string>
|
||||||
<string name="event_sync_status_error">Fel</string>
|
<string name="event_sync_status_error">Fel</string>
|
||||||
<string name="event_sync_status_completed">Slutfört</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>
|
</resources>
|
||||||
|
|||||||
@@ -444,4 +444,5 @@
|
|||||||
<string name="active_for_global">Ulimwenguni</string>
|
<string name="active_for_global">Ulimwenguni</string>
|
||||||
<string name="active_for_search">Tafuta</string>
|
<string name="active_for_search">Tafuta</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -370,4 +370,5 @@
|
|||||||
<string name="groups_no_descriptor">இந்த அரட்டைக் குழுவின் விளக்கமும் விதிகளும் இன்னும் சேர்க்கபடவில்லை. உரிமையாளரை அணுகி அவற்றை சேர்க்க கோரவும் </string>
|
<string name="groups_no_descriptor">இந்த அரட்டைக் குழுவின் விளக்கமும் விதிகளும் இன்னும் சேர்க்கபடவில்லை. உரிமையாளரை அணுகி அவற்றை சேர்க்க கோரவும் </string>
|
||||||
<string name="community_no_descriptor">இந்த சமூகத்தின் விளக்கமும் விதிகளும் இன்னும் சேர்க்கபடவில்லை. உரிமையாளரை அணுகி அவற்றை சேர்க்க கோரவும்</string>
|
<string name="community_no_descriptor">இந்த சமூகத்தின் விளக்கமும் விதிகளும் இன்னும் சேர்க்கபடவில்லை. உரிமையாளரை அணுகி அவற்றை சேர்க்க கோரவும்</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -815,4 +815,5 @@
|
|||||||
<string name="select_list_to_filter">แลือกลิสต์เพื่อโชว์</string>
|
<string name="select_list_to_filter">แลือกลิสต์เพื่อโชว์</string>
|
||||||
<string name="temporary_account">ออกจากระบบ</string>
|
<string name="temporary_account">ออกจากระบบ</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -168,4 +168,5 @@
|
|||||||
<string name="mark_all_as_read">Tümünü okundu olarak işaretle</string>
|
<string name="mark_all_as_read">Tümünü okundu olarak işaretle</string>
|
||||||
<string name="biometric_error">Hata</string>
|
<string name="biometric_error">Hata</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -500,4 +500,5 @@
|
|||||||
<string name="could_not_download_from_the_server">Не вдалося завантажити завантажені медіафайли з сервера</string>
|
<string name="could_not_download_from_the_server">Не вдалося завантажити завантажені медіафайли з сервера</string>
|
||||||
<string name="could_not_prepare_local_file_to_upload">Не вдалося підготувати локальний файл для завантаження: %1$s</string>
|
<string name="could_not_prepare_local_file_to_upload">Не вдалося підготувати локальний файл для завантаження: %1$s</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -69,4 +69,5 @@
|
|||||||
<string name="in_channel">"bu kanalda: "</string>
|
<string name="in_channel">"bu kanalda: "</string>
|
||||||
<string name="profile_banner">Profil banneri</string>
|
<string name="profile_banner">Profil banneri</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -12,4 +12,5 @@
|
|||||||
<string name="note_to_receiver">Lưu ý cho người nhận</string>
|
<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>
|
<string name="thank_you_so_much">Cảm ơn rất nhiều!</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1626,4 +1626,5 @@
|
|||||||
<string name="event_sync_status_downloading">下载中</string>
|
<string name="event_sync_status_downloading">下载中</string>
|
||||||
<string name="event_sync_status_error">错误</string>
|
<string name="event_sync_status_error">错误</string>
|
||||||
<string name="event_sync_status_completed">已完成</string>
|
<string name="event_sync_status_completed">已完成</string>
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -240,4 +240,5 @@
|
|||||||
<string name="pledge_amount_in_sats">聰數量</string>
|
<string name="pledge_amount_in_sats">聰數量</string>
|
||||||
<string name="looking_for_event">"“正在查找事件%1$s”"</string>
|
<string name="looking_for_event">"“正在查找事件%1$s”"</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -763,4 +763,5 @@
|
|||||||
<string name="dvm_waiting_to_confirm_payment">正在等待 DVM 確認付款或返回結果</string>
|
<string name="dvm_waiting_to_confirm_payment">正在等待 DVM 確認付款或返回結果</string>
|
||||||
<string name="add_a_nip96_server">添加 NIP-96 服務器</string>
|
<string name="add_a_nip96_server">添加 NIP-96 服務器</string>
|
||||||
<!-- Kind display names for relay subscription filter chips -->
|
<!-- Kind display names for relay subscription filter chips -->
|
||||||
|
<!-- M3 Action Dialog titles -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -258,6 +258,7 @@
|
|||||||
<string name="error_loading_replies">"Error loading replies: "</string>
|
<string name="error_loading_replies">"Error loading replies: "</string>
|
||||||
<string name="try_again">Try again</string>
|
<string name="try_again">Try again</string>
|
||||||
<string name="notification_feed_is_empty">No notifications yet.</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="feed_is_empty">Feed is empty.</string>
|
||||||
<string name="refresh">Refresh</string>
|
<string name="refresh">Refresh</string>
|
||||||
<string name="created">created</string>
|
<string name="created">created</string>
|
||||||
@@ -1877,4 +1878,38 @@
|
|||||||
<string name="event_sync_status_downloading">Downloading</string>
|
<string name="event_sync_status_downloading">Downloading</string>
|
||||||
<string name="event_sync_status_error">Error</string>
|
<string name="event_sync_status_error">Error</string>
|
||||||
<string name="event_sync_status_completed">Completed</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>
|
</resources>
|
||||||
|
|||||||
+97
-3
@@ -23,13 +23,26 @@ package com.vitorpamplona.quartz.experimental.attestations.attestation
|
|||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.AttestationStatus
|
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.AttestationStatus
|
||||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.Validity
|
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.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.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
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.signers.eventTemplate
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
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.nip31Alts.alt
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
import kotlinx.serialization.json.JsonNull.content
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
class AttestationEvent(
|
class AttestationEvent(
|
||||||
@@ -39,7 +52,22 @@ class AttestationEvent(
|
|||||||
tags: Array<Array<String>>,
|
tags: Array<Array<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: HexKey,
|
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 validity() = tags.validity()
|
||||||
|
|
||||||
fun status() = tags.status()
|
fun status() = tags.status()
|
||||||
@@ -50,25 +78,91 @@ class AttestationEvent(
|
|||||||
|
|
||||||
fun request() = tags.request()
|
fun request() = tags.request()
|
||||||
|
|
||||||
|
fun requestId() = tags.requestId()
|
||||||
|
|
||||||
|
fun requestAddress() = tags.requestAddress()
|
||||||
|
|
||||||
fun isRevoked() = status() == AttestationStatus.REVOKED
|
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 {
|
companion object {
|
||||||
const val KIND = 31871
|
const val KIND = 31871
|
||||||
const val ALT_DESCRIPTION = "Attestation"
|
const val ALT_DESCRIPTION = "Attestation"
|
||||||
|
|
||||||
fun build(
|
fun buildEvent(
|
||||||
dTagId: String,
|
dTagId: String,
|
||||||
|
about: EventHintBundle<Event>,
|
||||||
content: String = "",
|
content: String = "",
|
||||||
validity: Validity? = null,
|
validity: Validity? = null,
|
||||||
status: AttestationStatus? = null,
|
status: AttestationStatus? = null,
|
||||||
validFrom: Long? = null,
|
validFrom: Long? = null,
|
||||||
validTo: Long? = null,
|
validTo: Long? = null,
|
||||||
requestAddress: String? = null,
|
requestAddress: EventHintBundle<AttestationRequestEvent>? = null,
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
initializer: TagArrayBuilder<AttestationEvent>.() -> Unit = {},
|
initializer: TagArrayBuilder<AttestationEvent>.() -> Unit = {},
|
||||||
) = eventTemplate(KIND, content, createdAt) {
|
) = eventTemplate(KIND, content, createdAt) {
|
||||||
alt(ALT_DESCRIPTION)
|
alt(ALT_DESCRIPTION)
|
||||||
dTag(dTagId)
|
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) }
|
validity?.let { validity(it) }
|
||||||
status?.let { status(it) }
|
status?.let { status(it) }
|
||||||
validFrom?.let { validFrom(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.ValidToTag
|
||||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.Validity
|
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.Validity
|
||||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.ValidityTag
|
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.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))
|
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>.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.validTo() = firstNotNullOfOrNull(ValidToTag::parse)
|
||||||
|
|
||||||
fun TagArray.request() = firstNotNullOfOrNull(RequestTag::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
|
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.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
|
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 {
|
companion object {
|
||||||
const val TAG_NAME = "request"
|
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.has(1)) { return null }
|
||||||
ensure(tag[0] == TAG_NAME) { return null }
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
ensure(tag[1].isNotEmpty()) { 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]
|
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
|
package com.vitorpamplona.quartz.experimental.attestations.request
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
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.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.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
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.signers.eventTemplate
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
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.nip31Alts.alt
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
import kotlin.let
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
class AttestationRequestEvent(
|
class AttestationRequestEvent(
|
||||||
@@ -37,15 +51,45 @@ class AttestationRequestEvent(
|
|||||||
tags: Array<Array<String>>,
|
tags: Array<Array<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: HexKey,
|
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 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 {
|
companion object {
|
||||||
const val KIND = 31872
|
const val KIND = 31872
|
||||||
const val ALT_DESCRIPTION = "Attestation Request"
|
const val ALT_DESCRIPTION = "Attestation Request"
|
||||||
|
|
||||||
fun build(
|
fun buildEvent(
|
||||||
dTagId: String,
|
dTagId: String,
|
||||||
|
about: EventHintBundle<Event>,
|
||||||
content: String = "",
|
content: String = "",
|
||||||
attestorPubKeys: List<HexKey> = emptyList(),
|
attestorPubKeys: List<HexKey> = emptyList(),
|
||||||
cashuToken: String? = null,
|
cashuToken: String? = null,
|
||||||
@@ -54,6 +98,41 @@ class AttestationRequestEvent(
|
|||||||
) = eventTemplate(KIND, content, createdAt) {
|
) = eventTemplate(KIND, content, createdAt) {
|
||||||
alt(ALT_DESCRIPTION)
|
alt(ALT_DESCRIPTION)
|
||||||
dTag(dTagId)
|
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)
|
attestorPubKeys(attestorPubKeys)
|
||||||
cashuToken?.let { cashuToken(it) }
|
cashuToken?.let { cashuToken(it) }
|
||||||
initializer()
|
initializer()
|
||||||
|
|||||||
+12
@@ -21,10 +21,22 @@
|
|||||||
package com.vitorpamplona.quartz.experimental.attestations.request
|
package com.vitorpamplona.quartz.experimental.attestations.request
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.experimental.attestations.request.tags.CashuTokenTag
|
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.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
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
|
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>.attestorPubKeys(pubKeys: List<HexKey>) = addAll(pubKeys.map { PTag.assemble(it, null) })
|
||||||
|
|
||||||
fun TagArrayBuilder<AttestationRequestEvent>.cashuToken(token: String) = addUnique(CashuTokenTag.assemble(token))
|
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.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
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(
|
class LiveEventStore(
|
||||||
private val store: IEventStore,
|
private val store: IEventStore,
|
||||||
) {
|
) {
|
||||||
|
|||||||
+3
-7
@@ -29,14 +29,10 @@ import kotlinx.coroutines.cancel
|
|||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class manages per-connection subscriptions as coroutines. Each
|
* Represents a Nostr relay server that manages client connections, event storage, and verification.
|
||||||
* 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.
|
|
||||||
*
|
*
|
||||||
* The server is transport-agnostic: callers feed incoming JSON via
|
* This class acts as the central coordinator for a relay server, handling the lifecycle of [RelaySession]s
|
||||||
* [RelaySession.receive] and receive outgoing JSON via the [RelaySession.send] callback
|
* and providing access to the underlying event store.
|
||||||
* provided to [connect]. This allows use with any WebSocket library.
|
|
||||||
*
|
*
|
||||||
* @param store The [IEventStore] backing this relay.
|
* @param store The [IEventStore] backing this relay.
|
||||||
* @param policyBuilder Controls requirements for relay commands.
|
* @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